web-dev-qa-db-ja.com

LIMIT句に大きなオフセットを指定してMySQLクエリを高速化するにはどうすればよいですか?

大きなオフセットでmysqlLIMITSELECTingすると、パフォーマンスの問題が発生します。

SELECT * FROM table LIMIT m, n;

たとえば、オフセットmが1,000,000より大きい場合、操作は非常に遅くなります。

limit m, nを使用する必要があります。 id > 1,000,000 limit nのようなものは使用できません。

このステートメントを最適化してパフォーマンスを向上させるにはどうすればよいですか?

25
ZA.

おそらく、ターゲットテーブルのキーに関連するシーケンシャルキーを提供するインデックステーブルを作成できます。次に、このインデックステーブルをターゲットテーブルに結合し、where句を使用して、必要な行をより効率的に取得できます。

#create table to store sequences
CREATE TABLE seq (
   seq_no int not null auto_increment,
   id int not null,
   primary key(seq_no),
   unique(id)
);

#create the sequence
TRUNCATE seq;
INSERT INTO seq (id) SELECT id FROM mytable ORDER BY id;

#now get 1000 rows from offset 1000000
SELECT mytable.* 
FROM mytable 
INNER JOIN seq USING(id)
WHERE seq.seq_no BETWEEN 1000000 AND 1000999;
13
Paul Dixon

インターネットのどこかに、行の選択をできるだけコンパクトにする方法についてのブログ投稿があります。つまり、IDだけです。完全な結果を生成するには、必要なすべてのデータをフェッチする必要があります選択した行のみ

したがって、SQLは次のようになります(テストされていませんが、実際に効果があるかどうかはわかりません)。

select A.* from table A 
  inner join (select id from table order by whatever limit m, n) B
  on A.id = B.id
order by A.whatever

SQLエンジンが原始的すぎて、この種のSQLステートメントを許可できない場合、または何も改善されない場合は、この単一のステートメントを複数のステートメントに分割し、IDをデータ構造にキャプチャすることをお勧めします。

Update:私が話していたブログ投稿を見つけました:それはJeff Atwoodの "すべての抽象化は失敗した抽象化です" コーディングについてホラー。

9
bart

レコードが大きい場合は、データの読み込みが遅いことが原因である可能性があります。 id列にインデックスが付けられている場合は、それを選択するだけではるかに高速になります。次に、適切なIDのIN句を使用して2番目のクエリを実行できます(または、最初のクエリの最小IDと最大IDを使用してWHERE句を作成できます)。

スロー:

SELECT * FROM table ORDER BY id DESC LIMIT 10 OFFSET 50000

速い:

SELECT id FROM table ORDER BY id DESC LIMIT 10 OFFSET 50000

SELECT * FROM table WHERE id IN (1,2,3...10)
5
Scott Nelson

テーブルにすでにインデックスがある場合は、別のインデックスを作成する必要はないと思います。その場合は、この主キーで並べ替えてから、キーの値を使用して次の手順を実行できます。

SELECT * FROM myBigTable WHERE id > :OFFSET ORDER BY id ASC;

もう1つの最適化は、SELECT *を使用せず、IDのみを使用して、インデックスを読み取るだけで、すべてのデータを見つける必要がないようにすることです(IOオーバーヘッドを削減)。必要な場合他の列のいくつかは、おそらくこれらをインデックスに追加して、主キー(メモリに保持される可能性が高いため、ディスクルックアップを必要としない)で読み取られるようにすることができます-これはすべての場合に適切であるとは限りませんがだからあなたは遊びをしなければならないでしょう。

私はより詳細な記事を書きました:

http://www.4pmp.com/2010/02/scalable-mysql-avoid-offset-for-large-tables/

3
SlappyTheFish

Paul Dixonの答えは確かに問題の解決策ですが、シーケンステーブルを維持し、行のギャップがないことを確認する必要があります。

それが可能であれば、より良い解決策は、元のテーブルに行のギャップがなく、ID 1から開始することを確認することです。次に、ページ付けにIDを使用して行を取得します。

SELECT * FROMテーブルAWHERE id> = 1 AND id <= 1000;
SELECT * FROMテーブルAWHERE id> = 1001 AND id <= 2000;

等々...

2
Jackson Leung

私は最近この問題に遭遇しました。問題は修正すべき2つの部分でした。最初に、FROM句で内部選択を使用する必要がありました。これにより、主キーのみで制限とオフセットが行われました。

$subQuery = DB::raw("( SELECT id FROM titles WHERE id BETWEEN {$startId} AND {$endId}  ORDER BY title ) as t");  

次に、それをクエリのfrom部分として使用できます。

'titles.id',
                            'title_eisbns_concat.eisbns_concat', 
                            'titles.pub_symbol', 
                            'titles.title', 
                            'titles.subtitle', 
                            'titles.contributor1', 
                            'titles.publisher', 
                            'titles.epub_date', 
                            'titles.ebook_price', 
                            'publisher_licenses.id as pub_license_id', 
                            'license_types.shortname',
                            $coversQuery
                        )
                        ->from($subQuery)
                        ->leftJoin('titles',  't.id',  '=', 'titles.id')
                        ->leftJoin('organizations', 'organizations.symbol', '=', 'titles.pub_symbol') 
                        ->leftJoin('title_eisbns_concat', 'titles.id', '=', 'title_eisbns_concat.title_id') 
                        ->leftJoin('publisher_licenses', 'publisher_licenses.org_id', '=', 'organizations.id') 
                        ->leftJoin('license_types', 'license_types.id', '=', 'publisher_licenses.license_type_id')

このクエリを初めて作成したときは、MySqlでOFFSETとLIMITを使用していました。これは、100ページを超えるまでは正常に機能し、その後、オフセットが耐えられないほど遅くなり始めました。内部クエリでそれをBETWEENに変更すると、どのページでも高速になります。 MySqlがOFFSETを高速化していない理由はわかりませんが、betweenはそれを巻き戻しているようです。

0
PhPGuy