MySQL でインデックスのチューニング入門

MySQL でクエリのチューニング

正確なベンチマークではないので、実行時間は参考値として書いておきます。

チューニングするテーブルの定義

mysql> DESC purchases;
+------------+----------+------+-----+---------+----------------+
| Field      | Type     | Null | Key | Default | Extra          |
+------------+----------+------+-----+---------+----------------+
| id         | int(11)  | NO   | PRI | NULL    | auto_increment |
| user_id    | int(11)  | YES  | MUL | NULL    |                |
| price      | int(11)  | YES  |     | 0       |                |
| created_at | datetime | YES  | MUL | NULL    |                |
| updated_at | datetime | YES  | MUL | NULL    |                |
| deleted_at | datetime | YES  | MUL | NULL    |                |
+------------+----------+------+-----+---------+----------------+
mysql> SELECT COUNT(1) FROM purchases;
+----------+
| count(1) |
+----------+
|   34,321 |
+----------+

チューニングするSQLは以下の通り。月次集計の分析用に書いた奴なので、ちょっとアプリケーション内で発行するクエリと比べると、実運用っぽくないが・・・。

SELECT
    T.CREATED_YM CREATED_YM
    ,COUNT(T.USER_ID) PU_COUNT
FROM
    (SELECT
        P.USER_ID USER_ID
        ,DATE_FORMAT(MIN(P.CREATED_AT), '%Y/%m/01') CREATED_YM
    FROM
        PURCHASES P
    GROUP BY
        P.USER_ID
    ) T
GROUP BY
    T.CREATED_YM
ORDER BY
    T.CREATED_YM
;

レコードが34,000件程度なのでそんなに数がないが、実行時間は 1 min 22.77 sec くらい。

チューニング前の実行計画 (EXPLAIN)

EXPLAIN を頭に付けて、実行計画を確認すると・・・

+----+-------------+------------+-------+---------------+---------+---------+------+-------+---------------------------------+
| id | select_type | table      | type  | possible_keys | key     | key_len | ref  | rows  | Extra                           |
+----+-------------+------------+-------+---------------+---------+---------+------+-------+---------------------------------+
|  1 | PRIMARY     | <derived2> | ALL   | NULL          | NULL    | NULL    | NULL |  9402 | Using temporary; Using filesort |
|  2 | DERIVED     | P          | index | NULL          | user_id | 5       | NULL | 31343 |                                 |
+----+-------------+------------+-------+---------------+---------+---------+------+-------+---------------------------------+

user_id のインデックスでとりあえず頑張ってくれている模様。

“適切な” インデックスを作成してみる

現状を SHOW INDEX で確認すると・・・

mysql> SHOW INDEX FROM purchases;
+-----------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table     | Non_unique | Key_name   | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-----------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| purchases |          0 | PRIMARY    |            1 | id          | A         |       32871 |     NULL | NULL   |      | BTREE      |         |               |
| purchases |          1 | user_id    |            1 | user_id     | A         |       32871 |     NULL | NULL   | YES  | BTREE      |         |               |
| purchases |          1 | created_at |            1 | created_at  | A         |       32871 |     NULL | NULL   | YES  | BTREE      |         |               |
| purchases |          1 | updated_at |            1 | updated_at  | A         |       32871 |     NULL | NULL   | YES  | BTREE      |         |               |
+-----------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

SQL で使用しているカラムのuser_id, created_at それぞれにインデックスがあるが、1つしか使えないのでうまく最適化されない。複合インデックスを作成すると解消できる。ただし、where / group by 指定のカラムをまとめて指定するかつ順番にも注意が必要。Covering Index で調べると良い事あるかも。

mysql> ALTER TABLE  purchases ADD INDEX created_at_group_by_user_id(id, created_at);
mysql> SHOW INDEX FROM purchases;
+-----------+------------+-----------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table     | Non_unique | Key_name                    | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-----------+------------+-----------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| purchases |          0 | PRIMARY                     |            1 | id          | A         |       32871 |     NULL | NULL   |      | BTREE      |         |               |
| purchases |          1 | user_id                     |            1 | user_id     | A         |       32871 |     NULL | NULL   | YES  | BTREE      |         |               |
| purchases |          1 | created_at                  |            1 | created_at  | A         |       32871 |     NULL | NULL   | YES  | BTREE      |         |               |
| purchases |          1 | updated_at                  |            1 | updated_at  | A         |       32871 |     NULL | NULL   | YES  | BTREE      |         |               |
| purchases |          1 | created_at_group_by_user_id |            1 | user_id     | A         |         200 |     NULL | NULL   | YES  | BTREE      |         |               |
| purchases |          1 | created_at_group_by_user_id |            2 | created_at  | A         |         200 |     NULL | NULL   | YES  | BTREE      |         |               |
+-----------+------------+-----------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

もう一度、さっきのSQL を実行すると、0.26 sec になった。ちなみに、実行計画を確認すると・・・

+----+-------------+------------+-------+---------------+-----------------------------+---------+------+------+---------------------------------+
| id | select_type | table      | type  | possible_keys | key                         | key_len | ref  | rows | Extra                           |
+----+-------------+------------+-------+---------------+-----------------------------+---------+------+------+---------------------------------+
|  1 | PRIMARY     | <derived2> | ALL   | NULL          | NULL                        | NULL    | NULL | 9402 | Using temporary; Using filesort |
|  2 | DERIVED     | E          | range | NULL          | created_at_group_by_user_id | 14      | NULL |  201 | Using index for group-by        |
+----+-------------+------------+-------+---------------+-----------------------------+---------+------+------+---------------------------------+

参考例が分析用でそもそもSQL 自体にちょっとよろしくない部分があるので、EXPLAIN を見ながらどんなインデックスを設定するか考える参考になれば・・・。