Mysqlページングの実装と最適化

9434 ワード

通常、ORDER BY LIMIT start,offset方式でページングクエリーを行います.たとえば、次のSQLです.
SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 100, 10;

または、次のような条件なしのページングSQL:
SELECT * FROM `t1` ORDER BY id DESC LIMIT 100, 10;

一般に、ページングSQLの消費時間はstart値の増加に伴って急激に増加します.次の2つの異なる開始値のページングSQLの実行時間を見てみましょう.
[email protected]> SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 500, 10;
…

10 rows in set (0.05 sec)


[email protected]> SELECT * FROM `t1` WHERE ftype=6 ORDER BY id DESC LIMIT 935500, 10;
…

10 rows in set (2.39 sec)

ページ数が増えるにつれて、SQLクエリーの消費時間も数十倍に増え、明らかに科学的ではないことがわかります.今日は、このページングスキームを最適化する方法を分析します.一般的には、ページングを最適化する究極の方法は、ページングがなく、ハハハ~~~、くだらないことを言わないでください.確かに、ページングアルゴリズムをSphinx、Lucenceなどの第三者ソリューションに渡すことができ、MySQLに苦手なことをさせる必要はありません.もちろん、第三者を使うのは面倒だという友达がいます.私たちはMySQLでこのページを作りたいと思っています.どうすればいいですか.急いではいけません.ゆっくり分析して、まず次の表DDL、データ量、SQLの実行計画などの情報を調べます.
[email protected]> SHOW CREATE TABLE `t1`;
CREATE TABLE `t1` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
...
 `ftype` tinyint(3) unsigned NOT NULL,
...
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

[email protected]> select count(*) from t1;
+----------+
| count(*) |
+----------+
| 994584 |
+----------+

[email protected]> EXPLAIN SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 500, 10\G
*************************** 1. row ***************************
 id: 1
 select_type: SIMPLE
 table: t1
 type: index
possible_keys: NULL
 key: PRIMARY
 key_len: 4
 ref: NULL
 rows: 510
 Extra: Using where

[email protected]> EXPLAIN SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500, 10\G
*************************** 1. row ***************************
 id: 1
 select_type: SIMPLE
 table: t1
 type: index
possible_keys: NULL
 key: PRIMARY
 key_len: 4
 ref: NULL
 rows: 935510
 Extra: Using where

プライマリ・キー・インデックスでスキャンされたが、2番目のSQLでスキャンする必要があるレコードの数が大きすぎて、約935510件のレコードをスキャンしてから、ソート結果に基づいて10件のレコードを取得する必要があることがわかります.これは非常に遅いに違いありません.このような状況に対して、私たちの最適化の構想は比較的にはっきりしていて、2つの点があります.
1、             ,               
2、           ,           ,    N     

これにより、サブクエリ、テーブル接続の2つの適切な書き換え方法があります.すなわち、次のようになります.
#          ,              id,     ,  10    
#       2    ,    LIMIT start  ,       10, 935510,            
[email protected]> EXPLAIN SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC\G
*************************** 1. row ***************************
 id: 1
 select_type: PRIMARY
 table: 
 type: ALL
possible_keys: NULL
 key: NULL
 key_len: NULL
 ref: NULL
 rows: 10
 Extra: Using filesort
*************************** 2. row ***************************
 id: 2
 select_type: DERIVED
 table: t1
 type: ALL
possible_keys: PRIMARY
 key: NULL
 key_len: NULL
 ref: NULL
 rows: 973192
 Extra: Using where
*************************** 3. row ***************************
 id: 3
 select_type: SUBQUERY
 table: t1
 type: index
possible_keys: NULL
 key: PRIMARY
 key_len: 4
 ref: NULL
 rows: 935511
 Extra: Using where

#  INNER JOIN  ,JOIN           ID  ,              ,      10
[email protected]> EXPLAIN SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500,10) t2 USING (id)\G
*************************** 1. row ***************************
 id: 1
 select_type: PRIMARY
 table: 
 type: ALL
possible_keys: NULL
 key: NULL
 key_len: NULL
 ref: NULL
 rows: 935510
 Extra: NULL
*************************** 2. row ***************************
 id: 1
 select_type: PRIMARY
 table: t1
 type: eq_ref
possible_keys: PRIMARY
 key: PRIMARY
 key_len: 4
 ref: t2.id
 rows: 1
 Extra: NULL
*************************** 3. row ***************************
 id: 2
 select_type: DERIVED
 table: t1
 type: index
possible_keys: NULL
 key: PRIMARY
 key_len: 4
 ref: NULL
 rows: 973192
 Extra: Using where

次に、この2つの最適化された新しいSQLの実行時間を比較します.
[email protected]> SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) T ORDER BY id DESC;
...
rows in set (1.86 sec)
#       , profiling     ,       SQL  :28.2%

[email protected]> SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500,10) t2 USING (id);
...
10 rows in set (1.83 sec)
#  INNER JOIN  , profiling     ,       SQL  :30.8%

フィルタなしのページングSQLの比較を見てみましょう.
#  SQL
[email protected]> EXPLAIN SELECT * FROM `t1` ORDER BY id DESC LIMIT 935500, 10\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 935510
        Extra: NULL

[email protected]> SELECT * FROM `t1` ORDER BY id DESC LIMIT 935500, 10;
...
10 rows in set (2.22 sec)

#       
[email protected]> EXPLAIN SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC;
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: 
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 10
        Extra: Using filesort
*************************** 2. row ***************************
           id: 2
  select_type: DERIVED
        table: t1
         type: ALL
possible_keys: PRIMARY
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 973192
        Extra: Using where
*************************** 3. row ***************************
           id: 3
  select_type: SUBQUERY
        table: t1
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 935511
        Extra: Using index

[email protected]> SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC;
…
10 rows in set (2.01 sec)
#       , profiling     ,       SQL  :10.6%


#  INNER JOIN  
[email protected]> EXPLAIN SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1`ORDER BY id DESC LIMIT 935500,10) t2 USING (id)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: 
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 935510
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
        table: t1
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: t1.id
         rows: 1
        Extra: NULL
*************************** 3. row ***************************
           id: 2
  select_type: DERIVED
        table: t1
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 973192
        Extra: Using index

[email protected]> SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1`ORDER BY id DESC LIMIT 935500,10) t2 USING (id);
…
10 rows in set (1.70 sec)
#  INNER JOIN  , profiling     ,       SQL  :30.2%

ここで,サブクエリやINNER JOINを用いて最適化を行った場合,いずれも大幅に向上していることが分かるが,この手法は小さいページングにも同様に適用され,LIMIT開始のstart位置はかなり小さくなり,SQL実行時間もかなり速くなったが,この手法を用いると,WHERE条件付きページングはそれぞれクエリ効率を向上させることができる:24.9%,156.5%,WHERE条件なしページングはそれぞれクエリ効率を向上させる:554.5%,11.7%,皆さんは自分でテスト検証を行うことができます.スケールを上げるだけでは、これらの最適化方法が様々なページングモードに適用できることを確保し、最初から使用することができます.さまざまなシーンに対応するアップグレードの割合を見てみましょう.
 
大きなページ、WHERE付き
大きいページ、WHEREを持っていません
大ページング平均アップスケール
小さいページ、WHERE付き
小さいページ、WHEREを持っていません
全体平均上昇率
サブクエリの最適化
28.20%
10.60%
19.40%
24.90%
554.40%
154.53%
INNER JOIN最適化
30.80%
30.20%
30.50%
156.50%
11.70%
57.30%
結論:このように見れば明らかであり,特に大きなページングの場合にはINNER JOIN方式を用いてページングアルゴリズムを最適化することを優先的に推奨する.
上記のテストのたびにmysqldインスタンスを再起動し、SQL_を追加しました.NO_CACHEは、毎回直接データファイルまたはインデックスファイルで読み取ることを保証します.データが予熱されると、クエリーの効率はある程度向上しますが、上記の対応する効率の向上割合はほぼ一致します.