とあるMySQLのslowlogに残っていたところから見つけたクエリの書き換え。
サービスのどこで使われているものかまで詳しくみていないんだけど
CREATE TABLE `category2item` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`category_id` int(10) unsigned NOT NULL,
`subcategory_id` int(10) unsigned NOT NULL,
`item_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `subcategory_id` (`subcategory_id`,`item_id`),
KEY `picture_id` (`item_id`),
KEY `category_id` (`category_id`,`item_id`)
) ENGINE=InnoDB AUTO_INCREMENT=42651972 DEFAULT CHARSET=utf8
カテゴリーとアイテムを結びつけるテーブルがあって、そこに対して、ユニークなアイテム数をカウントするクエリを実行する。
mysql> SELECT COUNT(DISTINCT item_id) FROM category2item WHERE category_id = '2';
+----------------------------+
| COUNT(DISTINCT picture_id) |
+----------------------------+
| 2388652 |
+----------------------------+
1 row in set (3.02 sec)
件数がそれなり多いので、3秒以上掛かる。
slowlogにも多く残っていたので書き換えをしてみた。それがこれ。
mysql> SELECT COUNT(*) FROM (SELECT item_id FROM category2item WHERE category_id = '2' GROUP BY item_id) AS t;
+-------------------------+
| COUNT(DISTINCT item_id) |
+-------------------------+
| 2388652 |
+-------------------------+
1 row in set (1.26 sec)
大体1/2の時間になった。このクエリではユニークな件数取得のために DISTINCT ではなく、GROUP BY してユニークなIDリストを取得して、その結果を COUNT(*) している。
なんでこれが速くなるのかよくわからなかったんだけど、EXPLAIN をしてみたところ
mysql> EXPLAIN SELECT COUNT(*) FROM (SELECT item_id FROM category2item WHERE category_id = '2' GROUP BY item_id) AS t;
+----+-------------+---------------+------+---------------+-------------+---------+------+---------+------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+------+---------------+-------------+---------+------+---------+------------------------------+
| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Select tables optimized away |
| 2 | DERIVED | category2item | ref | category_id | category_id | 4 | | 4409520 | Using where; Using index |
+----+-------------+---------------+------+---------------+-------------+---------+------+---------+------------------------------+
2 rows in set (1.25 sec)
Extra に 「Select tables optimized away」が付いていたので、 MyISAM のクエリ最適化が行われたと予想。
そこでプロファイリングを有効にしてクエリを実行。プロファイリングについてはnippondanjiさんのblogを参考にした
漢(オトコ)のコンピュータ道: プロファイリングで快適MySQLチューニング生活
mysql> SET profiling=1;
mysql> SELECT COUNT(*) ..
mysql> SHOW PROFILE SOURCE;
+---------------------------+----------+---------+---------------+-------------+
| Status | Duration | Source | Source_file | Source_line |
+---------------------------+----------+---------+---------------+-------------+
| starting | 0.000041 | NULL | NULL | NULL |
| Opening tables | 0.000007 | unknown | sql_base.cc | 4519 |
| System lock | 0.000002 | unknown | lock.cc | 258 |
| Table lock | 0.000031 | unknown | lock.cc | 269 |
| optimizing | 0.000007 | unknown | sql_select.cc | 833 |
| statistics | 0.000041 | unknown | sql_select.cc | 1024 |
| preparing | 0.000014 | unknown | sql_select.cc | 1046 |
| executing | 0.000004 | unknown | sql_select.cc | 1780 |
| Sorting result | 0.000002 | unknown | sql_select.cc | 2205 |
| Sending data | 0.510723 | unknown | sql_select.cc | 2338 |
| converting HEAP to MyISAM | 0.106450 | unknown | sql_select.cc | 10984 |
| Sending data | 0.641679 | unknown | sql_select.cc | 11049 |
| init | 0.000011 | unknown | sql_select.cc | 2528 |
| optimizing | 0.000005 | unknown | sql_select.cc | 833 |
| executing | 0.000011 | unknown | sql_select.cc | 1780 |
| end | 0.000002 | unknown | sql_select.cc | 2574 |
| query end | 0.000001 | unknown | sql_parse.cc | 5082 |
| freeing items | 0.000014 | unknown | sql_parse.cc | 6106 |
| removing tmp table | 0.003672 | unknown | sql_select.cc | 10916 |
| closing tables | 0.000003 | unknown | sql_select.cc | 10941 |
| logging slow query | 0.000002 | unknown | sql_parse.cc | 1723 |
| cleaning up | 0.000002 | unknown | sql_parse.cc | 1691 |
+---------------------------+----------+---------+---------------+-------------+
22 rows in set (0.00 sec)
「converting HEAP to MyISAM」が入っているのでやはり MyISAM が使われた模様。
試しに元の COUNT(DISTINCT item_id) のプロファイルをとってみると
mysql> SHOW PROFILE SOURCE;
+--------------------+----------+------------------+---------------+-------------+
| Status | Duration | Source_function | Source_file | Source_line |
+--------------------+----------+------------------+---------------+-------------+
| starting | 0.000032 | NULL | NULL | NULL |
| Opening tables | 0.000006 | unknown function | sql_base.cc | 4519 |
| System lock | 0.000003 | unknown function | lock.cc | 258 |
| Table lock | 0.000003 | unknown function | lock.cc | 269 |
| init | 0.000010 | unknown function | sql_select.cc | 2528 |
| optimizing | 0.000006 | unknown function | sql_select.cc | 833 |
| statistics | 0.000038 | unknown function | sql_select.cc | 1024 |
| preparing | 0.000009 | unknown function | sql_select.cc | 1046 |
| executing | 0.000022 | unknown function | sql_select.cc | 1780 |
| Sending data | 3.018044 | unknown function | sql_select.cc | 2338 |
| end | 0.001905 | unknown function | sql_select.cc | 2574 |
| removing tmp table | 0.000006 | unknown function | sql_select.cc | 10916 |
| end | 0.000003 | unknown function | sql_select.cc | 10941 |
| query end | 0.000002 | unknown function | sql_parse.cc | 5082 |
| freeing items | 0.000017 | unknown function | sql_parse.cc | 6106 |
| logging slow query | 0.000002 | unknown function | sql_parse.cc | 1723 |
| logging slow query | 0.000030 | unknown function | sql_parse.cc | 1733 |
| cleaning up | 0.000003 | unknown function | sql_parse.cc | 1691 |
+--------------------+----------+------------------+---------------+-------------+
18 rows in set (0.00 sec)
こんな感じ。「Sending data」の時間が大きくなっているところから、ユニークなIDを割り出す為のデータ走査に時間が取られたのかなと予想。
では、MyISAMに変換されなかったらどうなるかと思って、tmp_table_size を 64MB (元16MB) まであげてみると、
mysql> SELECT COUNT(*) FROM (SELECT picture_id FROM category2picture WHERE category_id = '2' GROUP BY picture_id) AS t;
+----------+
| COUNT(*) |
+----------+
| 2388710 |
+----------+
1 row in set (1.10 sec)
mysql> SHOW PROFILE SOURCE;
+--------------------+----------+------------------+---------------+-------------+
| Status | Duration | Source_function | Source_file | Source_line |
+--------------------+----------+------------------+---------------+-------------+
| starting | 0.000041 | NULL | NULL | NULL |
| Opening tables | 0.000006 | unknown function | sql_base.cc | 4519 |
| System lock | 0.000002 | unknown function | lock.cc | 258 |
| Table lock | 0.000029 | unknown function | lock.cc | 269 |
| optimizing | 0.000006 | unknown function | sql_select.cc | 833 |
| statistics | 0.000037 | unknown function | sql_select.cc | 1024 |
| preparing | 0.000014 | unknown function | sql_select.cc | 1046 |
| executing | 0.000004 | unknown function | sql_select.cc | 1780 |
| Sorting result | 0.000002 | unknown function | sql_select.cc | 2205 |
| Sending data | 1.099120 | unknown function | sql_select.cc | 2338 |
| init | 0.000012 | unknown function | sql_select.cc | 2528 |
| optimizing | 0.000004 | unknown function | sql_select.cc | 833 |
| executing | 0.000010 | unknown function | sql_select.cc | 1780 |
| end | 0.000002 | unknown function | sql_select.cc | 2574 |
| query end | 0.000002 | unknown function | sql_parse.cc | 5082 |
| freeing items | 0.000013 | unknown function | sql_parse.cc | 6106 |
| removing tmp table | 0.000015 | unknown function | sql_select.cc | 10916 |
| closing tables | 0.000002 | unknown function | sql_select.cc | 10941 |
| logging slow query | 0.000001 | unknown function | sql_parse.cc | 1723 |
| cleaning up | 0.001948 | unknown function | sql_parse.cc | 1691 |
+--------------------+----------+------------------+---------------+-------------+
20 rows in set (0.00 sec)
「converting HEAP to MyISAM」がなくなって「Sending data」が1回にまとまった分、MyISAMが使われるより若干早い感じ。HEAP(MEMORY)の場合もCOUNT(*)が最適化されるんですね。8.3.1.2. WHERE Clause Optimizationにも書いてあった
まぁ、これでも1秒以上クエリに掛かるので、Webアプリケーションの中から使うのは躊躇われるところ。どこで使われているかアプリケーションのソースコード読むかー。