いつものように気になったので意訳
これら書く問題というのはMySQLの最初の方にある問題なんだが、気をつけてクエリーを解析しないと知らないまま終わってしまうことだ。phpDay.it でこの落とし穴についてデモしてきたが、ブログの記事にするのもいいとおもったんだ。
問題をデモするための典型的な例として、売上げクエリーを考えよう。データは次の3つのテーブルに入っている。
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`)
)
そして十分な量のデータを入れた。
そして次のクエリーを考える。昨日にどれだけ液晶テレビが売れたのかを調べるクエリーだ。
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)
などが結合時に使われている。ではさらに詳細に見てみるとどうだろう。
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. 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
ここでちょっと思い出してみる。
`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と日付範囲をインデックスでマッチして関連する行を読み込むだけな単純な最適化もできない。
これは結合するときだけ影響する。範囲条件を最初のテーブル(そしてそれのみ)に限定すれば、期待通りに動く。
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
)のカラムを追加して、それで絞り込むことにすればよい。
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);
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つのカラムを結合で利用していることが分かる。
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かそれ以降のバージョンで解決されると言うことだ。または「それは解説されている振る舞いである」と言うことらしい。でも実際には仕様ではなく致命的なバグであり、修正が必要だ。
comments powered by Disqus