Tweetいつものように気になったので意訳
これら書く問題というのはMySQLの最初の方にある問題なんだが、気をつけてクエリーを解析しないと知らないまま終わってしまうことだ。phpDay.it でこの落とし穴についてデモしてきたが、ブログの記事にするのもいいとおもったんだ。
問題をデモするための典型的な例として、売上げクエリーを考えよう。データは次の3つのテーブルに入っている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | CREATE TABLE `products` ( `prd_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `prd_name` VARCHAR(32) NOT NULL, PRIMARY KEY (`prd_id`), KEY `name` (`prd_name`) ) CREATE TABLE `tags` ( `tag_prd_id` INT(10) UNSIGNED NOT NULL, `tag_name` VARCHAR(32) NOT NULL, PRIMARY KEY (`tag_name`, `tag_prd_id`) ) CREATE TABLE `items_ordered` ( `itm_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `itm_prd_id` INT(10) UNSIGNED NOT NULL, `itm_order_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`itm_id`), KEY `itm_prd_id__and__itm_order_timestamp` (`itm_prd_id`,`itm_order_timestamp`) ) |
そして十分な量のデータを入れた。
そして次のクエリーを考える。昨日にどれだけ液晶テレビが売れたのかを調べるクエリーだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | SELECT COUNT(1) FROM tags t JOIN products p ON p.prd_id = t.tag_prd_id JOIN items_ordered i ON i.itm_prd_id = p.prd_id WHERE t.tag_name = 'lcd' AND i.itm_order_timestamp>= '2010-05-16 00:00:00' AND i.itm_order_timestamp <'2010-05-17 00:00:00' +----------+ | COUNT(1) | +----------+ | 4103 | +----------+ |
ちゃんとできたかな!
データ構造を見る限り、うまくいったように見える。tagsテーブルにはインデックスtag_nameがあり、items_orderedには(itm_prd_id, itm_order_timestamp)などが結合時に使われている。ではさらに詳細に見てみるとどうだろう。
1 2 3 4 5 6 7 8 9 10 11 12 | SHOW STATUS LIKE 'Handler_read%'; +-----------------------+--------+ | Variable_name | VALUE | +-----------------------+--------+ | Handler_read_first | 0 | | Handler_read_key | 3 | | Handler_read_next | 118181 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | +-----------------------+--------+ |
ちょっとよくないように見える。結果が4103行なのに、120000行もスキャンしている。必要なカラムにはちゃんとインデックスつけているのに!EXPLAINはどうなっているのか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | *************************** 1. ROW *************************** id: 1 select_type: SIMPLE TABLE: t TYPE: REF possible_keys: PRIMARY KEY: PRIMARY key_len: 98 REF: const ROWS: 1 Extra: USING WHERE; USING INDEX *************************** 2. ROW *************************** id: 1 select_type: SIMPLE TABLE: p TYPE: eq_ref possible_keys: PRIMARY KEY: PRIMARY key_len: 4 REF: example_db.t.tag_prd_id ROWS: 1 Extra: USING INDEX *************************** 3. ROW *************************** id: 1 select_type: SIMPLE TABLE: i TYPE: REF possible_keys: itm_prd_id__and__itm_order_timestamp KEY: itm_prd_id__and__itm_order_timestamp key_len: 4 REF: example_db.p.prd_id ROWS: 10325 Extra: USING WHERE; USING INDEX |
ここでちょっと思い出してみる。
1 2 3 | `itm_prd_id` INT(10) UNSIGNED NOT NULL `itm_order_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP KEY `itm_prd_id__and__itm_order_timestamp` (`itm_prd_id`,`itm_order_timestamp`) |
EXPLAINの3行目のkey_lenは4バイトなのだが、(itm_prd_id, itm_order_timestamp)は4+4で8バイトあるはずだ。と言うことは、1つのカラムしか見ていないことになる。
これをどう理解すべきか。データベースはlcdとタグの付いたソートされたデータを全て読み込む。それはやく120000行で、そのあと日付で絞り込まれる。なんと非効率な方法だ!MySQLは製品IDと日付範囲をインデックスでマッチして関連する行を読み込むだけな単純な最適化もできない。
これは結合するときだけ影響する。範囲条件を最初のテーブル(そしてそれのみ)に限定すれば、期待通りに動く。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | EXPLAIN SELECT COUNT(1) FROM items_ordered i WHERE i.itm_prd_id = 5 AND i.itm_order_timestamp>= '2010-05-16 00:00:00' AND i.itm_order_timestamp <'2010-05-17 00:00:00' *************************** 1. ROW *************************** id: 1 select_type: SIMPLE TABLE: i TYPE: range possible_keys: itm_prd_id__and__itm_order_timestamp KEY: itm_prd_id__and__itm_order_timestamp key_len: 8 REF: NULL ROWS: 1306 Extra: USING WHERE; USING INDEX |
この場合、結合しないのでMySQLはrefとは表示せず、key_lenも8バイトとなっている。つまりちゃんとインデックスが利用されていると言うことだ。
この問題に対しては多くの回避策があるが、基本的に状況に依存する。本質的には、結合から範囲条件を外すことになる。今回の場合は、日付(DATA)のカラムを追加して、それで絞り込むことにすればよい。
1 2 | ALTER TABLE items_ordered ADD itm_order_date DATE NOT NULL, ADD INDEX itm_prd_id__and__itm_order_date (itm_prd_id, itm_order_date); UPDATE items_ordered SET itm_order_date = DATE(itm_order_timestamp); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | EXPLAIN SELECT COUNT(1) FROM tags t JOIN products p ON p.prd_id = t.tag_prd_id JOIN items_ordered i ON i.itm_prd_id = p.prd_id WHERE t.tag_name = 'lcd' AND i.itm_order_date = '2010-05-16' *************************** 1. ROW *************************** id: 1 select_type: SIMPLE TABLE: t TYPE: REF possible_keys: PRIMARY KEY: PRIMARY key_len: 98 REF: const ROWS: 1 Extra: USING WHERE; USING INDEX *************************** 2. ROW *************************** id: 1 select_type: SIMPLE TABLE: p TYPE: eq_ref possible_keys: PRIMARY KEY: PRIMARY key_len: 4 REF: example_db.t.tag_prd_id ROWS: 1 Extra: USING INDEX *************************** 3. ROW *************************** id: 1 select_type: SIMPLE TABLE: i TYPE: REF possible_keys: itm_prd_id__and__itm_order_timestamp,itm_prd_id__and__itm_order_date KEY: itm_prd_id__and__itm_order_date key_len: 7 REF: example_db.p.prd_id,const ROWS: 206494 Extra: USING WHERE; USING INDEX |
このクエリーはitm_prd_id__and__itm_order_dateで7バイトとなっているので、(itm_prd_id, itm_order_date)をちゃんと利用していることが分かる。またrefでも2つのカラムを結合で利用していることが分かる。
1 2 3 4 5 6 7 8 9 10 11 | SHOW STATUS LIKE 'Handler_read%'; +-----------------------+-------+ | Variable_name | VALUE | +-----------------------+-------+ | Handler_read_first | 0 | | Handler_read_key | 3 | | Handler_read_next | 4104 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | +-----------------------+-------+ |
非常によくなったことが分かるだろう。
しかし、クエリーが異なれば、解決方法も異なることは覚えておいて欲しい。
この問題に対するバグレポートがいくつかある(例えば #8569, #19548)が、MySQLからの回答では6.0かそれ以降のバージョンで解決されると言うことだ。または「それは解説されている振る舞いである」と言うことらしい。でも実際には仕様ではなく致命的なバグであり、修正が必要だ。
Tags: MySQL


