MySQLでORDER BYの項目にインデックスを適用してみる

MySQLで大量のデータを追加してみる
の続き

MySQLでORDER BYの項目にインデックスを適用してみる

今回使用するテーブル

サンプルのテーブルに、レコードが1000万件入った状態。
pointには1〜100、 typeには1〜10、flagには0,1が入っている。

mysql> show create table sample;
+--------+---------------------------------------------------+
| Table  | Create Table                                      |
+--------+---------------------------------------------------+
| sample | CREATE TABLE `sample` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) NOT NULL,
  `point` int(11) NOT NULL,
  `type` smallint(6) NOT NULL,
  `flag` tinyint(1) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10026856 DEFAULT CHARSET=utf8 |
+--------+---------------------------------------------------+
mysql> select count(*) from sample;
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
mysql> select * from sample limit 20;
+----+-----------+-------+------+------+
| id | name      | point | type | flag |
+----+-----------+-------+------+------+
|  1 | sato      |    24 |    5 |    1 |
|  2 | suzuki    |    75 |    9 |    1 |
|  3 | takahashi |     5 |    6 |    0 |
|  4 | tanaka    |    90 |   10 |    0 |
|  5 | ito       |    41 |    2 |    1 |
|  6 | yamamoto  |    99 |    9 |    0 |
|  7 | watanabe  |    96 |    6 |    1 |
|  8 | nakamura  |    90 |    8 |    1 |
|  9 | kobayashi |    66 |    4 |    1 |
| 10 | kato      |    18 |    4 |    0 |
| 11 | sato      |    70 |    1 |    0 |
| 12 | suzuki    |    44 |    9 |    1 |
| 13 | takahashi |    33 |    7 |    0 |
| 14 | tanaka    |     4 |   10 |    1 |
| 15 | ito       |    41 |    1 |    0 |
| 16 | yamamoto  |    13 |    5 |    1 |
| 17 | watanabe  |    13 |   10 |    0 |
| 18 | nakamura  |    26 |    7 |    1 |
| 19 | kobayashi |    31 |    5 |    1 |
| 20 | kato      |    36 |    1 |    0 |
+----+-----------+-------+------+------+

このテーブルを使って、order byのインデックスを試してみる。

order byの項目でインデックスを作成してみる

1件とるだけなら早い検索も

mysql> select * from sample limit 1;
+----+------+-------+------+------+
| id | name | point | type | flag |
+----+------+-------+------+------+
|  1 | sato |    24 |    5 |    1 |
+----+------+-------+------+------+
1 row in set (0.00 sec)

order byをつけるととたんに遅くなる。

mysql> select * from sample order by point limit 1;
+----+----------+-------+------+------+
| id | name     | point | type | flag |
+----+----------+-------+------+------+
| 78 | nakamura |     1 |   10 |    1 |
+----+----------+-------+------+------+
1 row in set (5.08 sec)

インデックスを追加すると、

mysql> alter table sample add index idx1 (point);

0.01秒で検索できるようになった。

mysql> select * from sample order by point limit 1;
+----+----------+-------+------+------+
| id | name     | point | type | flag |
+----+----------+-------+------+------+
| 78 | nakamura |     1 |   10 |    1 |
+----+----------+-------+------+------+
1 row in set (0.01 sec)

explainの結果を見ても、インデックスが使われているのが分かる。

mysql> explain select * from sample order by point limit 1;
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | sample | index | NULL          | idx1 | 4       | NULL |    1 |       |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+

order byの一部のみインデックスを適用することはできない

pointのインデックスが作成された状態で、pointとtypeをorder byに指定すると
インデックスが効かない。

mysql> select * from sample order by point, type limit 1;
+-----+-----------+-------+------+------+
| id  | name      | point | type | flag |
+-----+-----------+-------+------+------+
| 499 | kobayashi |     1 |    1 |    0 |
+-----+-----------+-------+------+------+
1 row in set (5.30 sec)

explainの結果を見ても、インデックスが効いてないのがわかる。

mysql> explain select * from sample order by point, type limit 1;
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows     | Extra          |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+
|  1 | SIMPLE      | sample | ALL  | NULL          | NULL | NULL    | NULL | 10000571 | Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+

pointとtypeへの複合インデックスを作成する。

mysql> alter table sample add index idx2 (point, type);
mysql> show index from sample;
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table  | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| sample |          0 | PRIMARY  |            1 | id          | A         |    10000461 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx1     |            1 | point       | A         |          18 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx2     |            1 | point       | A         |          18 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx2     |            2 | type        | A         |        2650 |     NULL | NULL   |      | BTREE      |         |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

0.01秒で検索できるようになった。

mysql> select * from sample order by point, type limit 1;
+-----+-----------+-------+------+------+
| id  | name      | point | type | flag |
+-----+-----------+-------+------+------+
| 499 | kobayashi |     1 |    1 |    0 |
+-----+-----------+-------+------+------+
1 row in set (0.01 sec)

explainの結果を見ても、インデックスが効いてるのがわかる。

mysql> explain select * from sample order by point, type limit 1;
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | sample | index | NULL          | idx2 | 6       | NULL |    1 |       |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+

複合インデックスの一部をorder byに適用することは可

一旦、pointのみのインデックスを削除して、
pointとtypeの複合インデックスのみにする。

mysql> alter table sample drop index idx1;
mysql> show index from sample;
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table  | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| sample |          0 | PRIMARY  |            1 | id          | A         |    10000461 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx2     |            1 | point       | A         |          18 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx2     |            2 | type        | A         |        5154 |     NULL | NULL   |      | BTREE      |         |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

この状態で、order byにpointのみを指定してもインデックスは効く。

mysql> select * from sample order by point limit 1;
+-----+-----------+-------+------+------+
| id  | name      | point | type | flag |
+-----+-----------+-------+------+------+
| 499 | kobayashi |     1 |    1 |    0 |
+-----+-----------+-------+------+------+
1 row in set (0.00 sec)
mysql> explain select * from sample order by point limit 1;
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | sample | index | NULL          | idx2 | 6       | NULL |    1 |       |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------+

order byの順にインデックスが作成されていないと効かない

複合インデックスが作成されていても、
複合インデックスの順にorder byが指定されないとインデックスは効かない。

point,typeの複合インデックスがある状態で、
order byにtype,pointと指定しても、インデックスは効かない。

mysql> select * from sample order by type, point limit 1;
+-----+-----------+-------+------+------+
| id  | name      | point | type | flag |
+-----+-----------+-------+------+------+
| 499 | kobayashi |     1 |    1 |    0 |
+-----+-----------+-------+------+------+
1 row in set (5.09 sec)
mysql> explain select * from sample order by type, point limit 1;
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows     | Extra          |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+
|  1 | SIMPLE      | sample | ALL  | NULL          | NULL | NULL    | NULL | 10000363 | Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+

ASCとDESCが混在しているとインデックスが効かない

複合インデックスが作成されていても、
ASCとDESCが混在しているとインデックスは効かない。

mysql> select * from sample order by point asc, type desc limit 1;
+----+----------+-------+------+------+
| id | name     | point | type | flag |
+----+----------+-------+------+------+
| 78 | nakamura |     1 |   10 |    1 |
+----+----------+-------+------+------+
1 row in set (5.08 sec)

両方descは問題ない。

mysql> select * from sample order by point desc, type desc limit 1;
+---------+-----------+-------+------+------+
| id      | name      | point | type | flag |
+---------+-----------+-------+------+------+
| 9999299 | kobayashi |   100 |   10 |    1 |
+---------+-----------+-------+------+------+
1 row in set (0.00 sec)

whereとorder byの項目に複合インデックス

一旦、インデックスを削除し、pointのインデックスのみにする。

mysql> alter table sample drop index idx2;
mysql> alter table sample add index idx1 (point);
mysql> show index from sample;
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table  | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| sample |          0 | PRIMARY  |            1 | id          | A         |    10000461 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx1     |            1 | point       | A         |          18 |     NULL | NULL   |      | BTREE      |         |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

この状態で、検索条件にpointを指定して件数をカウントすると、
当然のことながらインデックスが効く。

mysql> select count(*) from sample where point = 100;
+----------+
| count(*) |
+----------+
|    99450 |
+----------+
1 row in set (0.03 sec)

order byにtypeを指定すると、ソートが行われる分、少し検索に時間がかかるようになる。

mysql> select count(*) from sample where point = 100 order by type;
+----------+
| count(*) |
+----------+
|    99450 |
+----------+
1 row in set (0.29 sec)

pointとtypeの複合インデックスを作成すると、

mysql> alter table sample add index idx2 (point, type);

検索がすぐに終わる。

mysql> select count(*) from sample where point = 100 order by type;
+----------+
| count(*) |
+----------+
|    99450 |
+----------+
1 row in set (0.03 sec)

whereを指定した場合のインデックスの挙動がよく分からない

複合インデックスを削除して、pointのインデックスのみにする。

mysql> alter table sample drop index idx2;
mysql> show index from sample;
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table  | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| sample |          0 | PRIMARY  |            1 | id          | A         |    10000461 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx1     |            1 | point       | A         |          18 |     NULL | NULL   |      | BTREE      |         |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

この状態で、
pointでソートして件数をカウントすると、約2秒ほどかかる。
pointのインデックスが効いている。

mysql> select count(*) from sample order by point;
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (2.22 sec)
mysql> explain select count(*) from sample order by point;
+----+-------------+--------+-------+---------------+------+---------+------+----------+-------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows     | Extra       |
+----+-------------+--------+-------+---------------+------+---------+------+----------+-------------+
|  1 | SIMPLE      | sample | index | NULL          | idx1 | 4       | NULL | 10000402 | Using index |
+----+-------------+--------+-------+---------------+------+---------+------+----------+-------------+
1 row in set (0.00 sec)

条件にflag = 1を指定して検索すると、約3秒ほどかかる。
そして、インデックスは何も効かなくなる。

mysql> select count(*) from sample where flag = 1 order by point;
+----------+
| count(*) |
+----------+
|  5001302 |
+----------+
1 row in set (3.45 sec)
mysql> explain select count(*) from sample where flag = 1 order by point;
+----+-------------+--------+------+---------------+------+---------+------+----------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows     | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+----------+-------------+
|  1 | SIMPLE      | sample | ALL  | NULL          | NULL | NULL    | NULL | 10000402 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+----------+-------------+
1 row in set (0.00 sec)

次に、flagのインデックスを作成する。

mysql> alter table sample add index idx3 (flag);
mysql> show index from sample;
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table  | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| sample |          0 | PRIMARY  |            1 | id          | A         |    10000461 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx1     |            1 | point       | A         |        2044 |     NULL | NULL   |      | BTREE      |         |
| sample |          1 | idx3     |            1 | flag        | A         |          18 |     NULL | NULL   |      | BTREE      |         |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

前回と同様に、条件にflag = 1を指定して検索すると、
なぜか約20秒以上かかるようになった。

mysql> select count(*) from sample where flag = 1 order by point;
+----------+
| count(*) |
+----------+
|  5001302 |
+----------+
1 row in set (22.55 sec)

インデックスは、flagのインデックスが効いている。

mysql> explain select count(*) from sample where flag = 1 order by point;
+----+-------------+--------+------+---------------+------+---------+-------+---------+-------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref   | rows    | Extra |
+----+-------------+--------+------+---------------+------+---------+-------+---------+-------+
|  1 | SIMPLE      | sample | ref  | idx3          | idx3 | 1       | const | 5000230 |       |
+----+-------------+--------+------+---------------+------+---------+-------+---------+-------+

どういう仕様なんだろうか?
よく分からない。

【参考】
MySQL :: MySQL 4.1 リファレンスマニュアル :: 5.2.8 MySQL による ORDER BY の最適化
http://dev.mysql.com/doc/refman/4.1/ja/order-by-optimisation.html

漢(オトコ)のコンピュータ道: オトコのソートテクニック2008
http://nippondanji.blogspot.jp/2008/12/2008.html