MySQLでは、並べ替えにorder byを使用し、ページングに制限を使用することがよくあります。最初に並べ替えてからページングする必要がある場合は、「select * from table name order by sort field limit M、N」と同様の記述を使用することがよくあります。しかし、この書き方は、より深い使用の罠を隠します。ソートフィールドでのデータ重複の場合、ソート結果が期待と矛盾する可能性があります。
1.異常現象
たとえば、バージョン5.6.17のMySQLデータベースには、tbl_mgm_tourテーブルがあり、テーブル構造は次のとおりです。
mysql> show full columns from tbl_mgm_tour;
+---------+--------------+-----------------+------+-----+---------+-------+---------------------------------+--------------+
| Field | Type | Collation | Null | Key | Default | Extra | Privileges | Comment |
+---------+--------------+-----------------+------+-----+---------+-------+---------------------------------+--------------+
| tour_id | char(15) | utf8_general_ci | NO | PRI | | | select,insert,update,references | 景区编号 |
| name | varchar(100) | utf8_general_ci | NO | | | | select,insert,update,references | 景区名称 |
| grade | varchar(10) | utf8_general_ci | NO | | | | select,insert,update,references | 景区等级 |
+---------+--------------+-----------------+------+-----+---------+-------+---------------------------------+--------------+
3 rows in set (0.03 sec)
テーブルデータは次のとおりです。
mysql> select * from tbl_mgm_tour;
+---------+----------------------------------------------+-------+
| tour_id | name | grade |
+---------+----------------------------------------------+-------+
| 001 | 东方明珠广播电视塔 | 5A |
| 002 | 上海野生动物园 | 5A |
| 003 | 上海科技馆 | 5A |
| 005 | 上海博物馆 | 4A |
| 006 | 上海佘山国家森林公园·东佘山园 | 4A |
| 007 | 上海佘山国家森林公园·西佘山园 | 4A |
| 008 | 上海豫园 | 4A |
| 009 | 金茂大厦88层观光厅 | 4A |
| 056 | 上海南汇桃花村 | 3A |
| 057 | 大宁郁金香公园 | 3A |
| 058 | 东方假日田园 | 3A |
| 059 | 廊下生态园 | 3A |
| 060 | 中国农民画村 | 3A |
+---------+----------------------------------------------+-------+
13 rows in set (0.00 sec)
ここで、景勝地レベルに従って降順でtbl_mgm_tourテーブルをクエリし、ページごとに5つのエントリを使用してページごとにクエリを実行します。sqlステートメントは次のように簡単に記述できます。
SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 0, 5;
クエリの実行中に、データの最初のページをクエリすると、結果は次のようになります。
mysql> SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 0, 5;
+---------+----------------------------------------------+-------+
| tour_id | name | grade |
+---------+----------------------------------------------+-------+
| 001 | 东方明珠广播电视塔 | 5A |
| 002 | 上海野生动物园 | 5A |
| 003 | 上海科技馆 | 5A |
| 006 | 上海佘山国家森林公园·东佘山园 | 4A |
| 007 | 上海佘山国家森林公园·西佘山园 | 4A |
+---------+----------------------------------------------+-------+
5 rows in set (0.00 sec)
2ページ目のデータをクエリすると、結果は次のようになります。
mysql> SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 5, 5;
+---------+----------------------------------------------+-------+
| tour_id | name | grade |
+---------+----------------------------------------------+-------+
| 007 | 上海佘山国家森林公园·西佘山园 | 4A |
| 006 | 上海佘山国家森林公园·东佘山园 | 4A |
| 005 | 上海博物馆 | 4A |
| 060 | 中国农民画村 | 3A |
| 057 | 大宁郁金香公园 | 3A |
+---------+----------------------------------------------+-------+
5 rows in set (0.00 sec)
tbl_mgm_tourテーブルには13個のデータと3ページのデータがありますが、実際のクエリプロセスでは、同じデータが最初のページと2番目のページに表示されます。
2.異常分析
どうしたの?上記のページングSQLは、最初にテーブルデータを並べ替えてから、ページングに対応するデータをフェッチしていませんか?
上記の実際の実装結果は、現実と想像の間にしばしばギャップがあることを証明しています。実際のSQL実行は、上記の方法では実行されません。実際、MySQLはLimitを最適化します。特定の最適化方法については、公式ドキュメントを参照してください:https://dev.mysql.com/doc/refman/5.7/en/limit-optimization.html(これはバージョンの説明です) 5.7)、抽出直接関連するいくつかのポイントを以下に説明します。
- LIMITrow_countをORDERBYと組み合わせると、MySQLは、結果全体を並べ替えるのではなく、並べ替えられた結果の最初のrow_count行が見つかるとすぐに並べ替えを停止します。インデックスを使用して順序付けを行う場合、これは非常に高速です。ファイルソートを実行する必要がある場合は、LIMIT句のないクエリに一致するすべての行が選択され、最初のrow_countが見つかる前に、それらのほとんどまたはすべてがソートされます。最初の行が見つかった後、MySQLは結果セットの残りをソートしません。
この動作の1つの兆候は、このセクションで後述するように、LIMITがある場合とない場合のORDERBYクエリが異なる順序で行を返す場合があることです。
上記の公式ドキュメントでは、Limitrowcountとorderbyを組み合わせると、MySQLはクエリ結果全体を並べ替えて返すのではなく、並べ替えられた行数を見つけてすぐに返すと述べています。インデックスで並べ替えると非常に高速になります。ファイルの並べ替えの場合、クエリに一致するすべての行(制限なし)が選択され、制限で必要な行数が次のようになるまで、選択した行のほとんどまたはすべてが並べ替えられます。見つかりました。limitで必要なrowcount行が見つかった場合、MySQLは結果セットの残りの行をソートしません。
ここでは、対応するSQLの実行プランを確認します。
mysql> EXPLAIN SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 0, 5;
+----+-------------+--------------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------------+------+---------------+------+---------+------+------+----------------+
| 1 | SIMPLE | tbl_mgm_tour | ALL | NULL | NULL | NULL | NULL | 13 | Using filesort |
+----+-------------+--------------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)
ファイルソートが使用されており、テーブルにインデックスが追加されていないことを確認できます。したがって、このSQLを実行すると、limitで必要な行が検出され、すぐにクエリ結果が返されます。
しかし、すぐに戻ったとしても、なぜページネーションが不正確なのですか?以下の指示は公式文書で行われます:
複数の行のORDERBY列の値が同じである場合、サーバーはそれらの行を任意の順序で自由に返すことができ、全体的な実行プランによって異なる場合があります。言い換えると、これらの行の並べ替え順序は、順序付けされていない列に関して非決定的です。
フィールドによる順序に同じ値の複数の行がある場合、MySQLは、対応する実行プランに応じて、クエリ結果をランダムな順序で返します。つまり、ソートされた列が順序付けされていない場合、ソートされた結果行の順序も不確実です。
これに基づいて、ソートするフィールドがグレードであり、同じグレード値を持つデータの行が数行しかないため、ページングが不正確である理由が基本的にわかります。実際の実行では、返された結果に対応する行の順序不確かです。上記の状況に対応して、最初のページに返される名前は「上海佘山国立森林公園・東佘山公園」であり、「上海佘山国立森林公園・西佘山公園」のデータが最初にランク付けされる可能性があります。 、上記の2つのデータ行はすぐ後ろにあるため、2番目のページが再び表示されます。
三、解決策
この状況はどのように解決する必要がありますか?公式の解決策は次のとおりです。
LIMITの有無にかかわらず同じ行の順序を確保することが重要な場合は、ORDER BY句に追加の列を含めて、順序を確定的にします。たとえば、id値が一意である場合、次のように並べ替えることで、特定のカテゴリ値の行をid順に表示できます。
Limitの有無にかかわらず同じソート結果を保証したい場合は、ソート条件を追加できます。たとえば、idフィールドが一意である場合、順序が安定していることを確認するために、ソートフィールドにIDソートを追加することを検討できます。
したがって、上記の場合、tbl_mgm_tourテーブルの主キーのtour_idフィールドなど、SQLに別の並べ替えフィールドを追加して、ページングの問題を解決できます。変更されたSQLは次のとおりです。
mysql> SELECT * FROM tbl_mgm_tour ORDER BY grade DESC, tour_id LIMIT 0, 5;
もう一度テストして問題を解決してください!
4、補足説明
異なるデータベースバージョンの同じデータの場合、並べ替えの結果は正常または異常である可能性があります。上記でテストしたデータベースのバージョンは5.6.17です。5.7.29バージョンのデータベースでテストした場合、並べ替えの結果は正常です。
参考記事: