web-dev-qa-db-ja.com

二重クエリなしのMySQLページネーション?

MySQLクエリから結果の数を取得し、同時に結果を制限する方法があるかどうか疑問に思っていました。

ページネーションの仕組み(私が理解しているように)、最初に次のようなことをします

query = SELECT COUNT(*) FROM `table` WHERE `some_condition`

Num_rows(query)を取得すると、多数の結果が得られます。しかし、実際に結果を制限するには、次のような2番目のクエリを実行する必要があります。

query2 = SELECT COUNT(*) FROM `table` WHERE `some_condition` LIMIT 0, 10

私の質問:とにかく、与えられる結果の総数を取得し、単一のクエリで返される結果を制限することはできますか?またはこれを行うより効率的な方法。ありがとう!

102
atp

いいえ、それはページネーションをしたいアプリケーションがどれだけ必要なのかです。クエリを2回作成しますが、信頼性と防弾性があります。ただし、数秒間、カウントをキャッシュできます。これは非常に役立ちます。

もう1つの方法は、_SQL_CALC_FOUND_ROWS_句を使用してからSELECT FOUND_ROWS()を呼び出すことです。後でFOUND_ROWS()呼び出しを行わなければならないという事実とは別に、これには問題があります:MySQLに バグがあります これは_ORDER BY_クエリに影響を与えるため、2つのクエリの単純なアプローチよりも大きなテーブルではるかに遅くなります。

61
staticsan

2つのクエリを実行することはほとんどありません。

必要以上の行を返すだけで、ページに10だけが表示されます。表示されている行よりも多い場合は、[次へ]ボタンを表示します。

SELECT x, y, z FROM `table` WHERE `some_condition` LIMIT 0, 11
// iterate through and display 10 rows.

// if there were 11 rows, display a "Next" button.

クエリは、最も関連性の高い順に返されます。ほとんどの人は、412から236ページに行くことを気にしないでしょう。

Google検索を実行し、結果が最初のページにない場合、9ページではなく2ページに移動する可能性があります。

64
Derrick

二重クエリを回避する別のアプローチは、最初にLIMIT句を使用して現在のページのすべての行をフェッチし、最大行数が取得された場合に2番目のCOUNT(*)クエリのみを実行することです。

多くのアプリケーションでは、最も可能性の高い結果は、すべての結果が1ページに収まることであり、ページネーションを行う必要があるのは標準ではなく例外です。これらの場合、最初のクエリは結果の最大数を取得しません。

たとえば、stackoverflowの質問に対する回答が2ページ目にめったにこぼれることはほとんどありません。回答に対するコメントが、すべてを表示するために必要な5程度を超えることはめったにありません。

したがって、これらのアプリケーションでは、最初にLIMITを使用してクエリを実行するだけでよく、その制限に到達しない限り、2番目のCOUNT(*)クエリを実行する必要なしに行数を正確に把握できます。大部分の状況をカバーします。

25
thomasrutter

ほとんどの場合、直感に反するように思われますが、1つのクエリで実行するよりも、2つのクエリで実行する方がはるかに高速でリソース集約的ではありません。

SQL_CALC_FOUND_ROWSを使用すると、大規模なテーブルの場合、2つのクエリ(COUNT(*)を使用したクエリとLIMITを使用したクエリ)を2つ実行する場合よりも、クエリが非常に遅くなります。これは、SQL_CALC_FOUND_ROWSによってLIMIT句が適用されるためafter行をフェッチする前ではなく、すべての可能な結果について行全体をフェッチするためです。制限を適用する前に。これは実際にはデータをフェッチするため、インデックスでは満足できません。

COUNT(*)をフェッチするだけで、実際のデータと実際のデータをフェッチしない2つのクエリアプローチをとる場合、通常インデックスを使用でき、実際の行データをフェッチする必要がないため、これははるかに速く満たすことができますそれが見るすべての行。次に、2番目のクエリは、最初の$ offset + $ limit行だけを見てから戻る必要があります。

MySQLパフォーマンスブログのこの投稿では、これについてさらに説明しています。

http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

ページネーションの最適化の詳細については、 this post および this post を確認してください。

15
thomasrutter

私の答えは遅れるかもしれませんが、2番目のクエリ(制限付き)をスキップして、バックエンドスクリプトで情報をフィルタリングするだけで済みます。たとえば、PHPでは、次のようなことができます。

if($queryResult > 0) {
   $counter = 0;
   foreach($queryResult AS $result) {
       if($counter >= $startAt AND $counter < $numOfRows) {
            //do what you want here
       }
   $counter++;
   }
}

しかし、もちろん、考慮すべきレコードが数千ある場合、非常に速く非効率になります。事前に計算されたカウントを調べることをお勧めします。

この件に関する良い読み物は次のとおりです。 http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf

2
Kama
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
2
Cris McLaughlin

クエリのほとんどをサブクエリで再利用して、識別子に設定できます。たとえば、実行時に文字「s」を含む映画を検索する映画クエリは、私のサイトでは次のようになります。

SELECT Movie.*, (
    SELECT Count(1) FROM Movie
        INNER JOIN MovieGenre 
        ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
    WHERE Title LIKE '%s%'
) AS Count FROM Movie 
    INNER JOIN MovieGenre 
    ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
WHERE Title LIKE '%s%' LIMIT 8;

私はデータベースの専門家ではないことに注意してください。誰かがそれをもう少し最適化できることを期待しています。 SQLコマンドラインインターフェイスから直接実行しているので、どちらもラップトップで約0.02秒かかります。

0
Philip Rollins