MySQL の SHOW STATUS LIKE 'handler_read%
で取れる統計値が何を意味する数値なのかまとめます
- Handler_read_first
- Handler_read_last
- Handler_read_key
- Handler_read_next
- Handler_read_prev
- Handler_read_rnd
- Handler_read_rnd_next
についてのモデル図とサンプルクエリを載せています
- 簡略化のために InnoDB の primary key (クラスタインデックス) だけ元に図にしています
- セカンダリインデックスも考えると大変そうなので ...
- モデル図は kazeburo さんのグレートスライド http://www.slideshare.net/kazeburo/isucon-summerclass2014action2final をまねて書いています
- モデル図はソースやマニュアル、B+木の仕組みから「おそらくこうだろう」という推測で書いています 👻 そのため正確さを欠いている点がある場合は了承ください (間違いはご指摘いただけるとありがたいです)
Handler_read_* の説明は MySQL :: MySQL 5.6 Reference Manual :: 5.1.6 Server Status Variables らへんを読むとよいでしょう。ただし、これ読んでも具体的な挙動が分からないってのが本音です ...
インデックスの最初のエントリを読み取るとカウントされる
昇順で (フル)インデックススキャン する際に Handler_read_next と一緒に使われます
インデックスの最後のエントリを読み取るとカウントされる
降順で (フル)インデックススキャン する際に Handler_read_prev と一緒に使われます
インデックスを使って対象のレコードを見つけるとカウントされる
SELECT * FROM foo WHERE id = 3
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | foo | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
クエリの種類として最速。
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
in
にマッチする行をそれぞれ取りにいって、 Handler_read_key でカウントされます
SELECT * FROM foo WHERE id in (2,4,6);
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | foo | range | PRIMARY | PRIMARY | 4 | NULL | 3 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 3 |
| Handler_read_last | 0 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
SELECT *
にすると、 in
を使っていても結果が異なります。フルテーブルスキャンになってしまいました
SELECT * FROM foo WHERE id in (2,4,6,8)
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | foo | ALL | PRIMARY | NULL | NULL | NULL | 8 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 9 |
+-----------------------+-------+
SELECT *
でかつ in
に指定したレコード数が、テーブル全体のレコード数の半数に達しているので、オプティマイザがフルテーブルスキャンの方がよいと選択したか?
( Handler_read_rnd_next の説明は別に書きます )
赤矢印とオレンジ矢印が Handler_read_next としてカウントされる動きです
上記は レンジスキャン になっているモデルとなります
- インデックスで対象のレコードを見つける
- Handler_read_key でカウントされます
- インデックスで昇順に次のレコードを探す (赤矢印)
- Handler_read_next でカウントされます
- B+木インデックスでは隣のリーフへのポインタが用意されている (赤矢印)
- このポインタを辿ることで 次のレコード を探すことができる
- range 検索では隣のリーフ(レコード)も range に収まるかどうかの判定が必要 (オレンジ矢印)
- Handler_read_next でカウントされます
- 隣のリーフが存在しない/マッチケースでも Handler_read_next がカウントされるのに注意。つまり Haandler_read_next は fetch するレコード数と一致しない
# 3,4,5,6 を返す
SELECT * FROM foo WHERE 2 < id and id < 7;
# BETWEEN 使っても同じ
SELECT * FROM foo WHERE id BETWEEN 3 AND 6
EXPLAIN は type = range
になっています
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | foo | range | PRIMARY | PRIMARY | 4 | NULL | 3 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 4 | # <= 読み取れる行より +1 多い
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
なお ORDER BY id DESC
にすると Handler_read_prev がカウントされます (降順にインデックスを辿るため)
昇順のフルインデックススキャンのモデルとなります。InnoDB の primary キーの場合は フルインデックススキャン = フルテーブルスキャン
に等しいはず (TODO)
id
が最小のキーを見つける
- Handler_read_first と Handler_read_key がカウントされる
- 範囲指定されていないのでインデックスを総なめする (赤矢印、オレンジ矢印)
- Handler_read_next がカウントされる
SELECT * FROM foo ORDER BY id ASC
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
| 1 | SIMPLE | foo | index | NULL | PRIMARY | 4 | NULL | 8 | NULL |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 8 | # <= 読み取れる行より +1 多い
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
SELECT id
にしてみるとどうなるか?
SELECT id FROM foo ORDER BY id ASC
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | foo | index | NULL | PRIMARY | 4 | NULL | 8 | Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 8 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
- Handler_read_next の数値は一緒だけど Using index が出る。
- セカンダリインデックスの場合は Covering Index となっていい感じのはず
- primary キーの場合はどうなんだっけ?
降順のフルインデックススキャンのモデルとなります。InnoDB の primary キーの場合は フルインデックススキャン = フルテーブルスキャン
に等しいはず
id
が最大のキーを見つける
- Handler_read_last ** と Handler_read_key がカウントされる
- インデックスを総なめする (赤矢印、オレンジ矢印)
- Handler_read_next がカウントされる
SELECT * FROM foo ORDER BY id DESC
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
| 1 | SIMPLE | foo | index | NULL | PRIMARY | 4 | NULL | 8 | NULL |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 1 |
| Handler_read_next | 0 |
| Handler_read_prev | 8 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
SELECT id FROM foo LIMIT 3 OFFSET 5
kazeburo さんのエントリにもある通り LIMIT + OFFSET 検索は効率が良くないことが Handler_read_next の数値から読み取れる
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | foo | index | NULL | PRIMARY | 4 | NULL | 8 | Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 7 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
インデックスを貼っていないカラムを指定した場合フルテーブルスキャンになります (*1)
この際に Handler_read_rnd_next がカウントされる様子。InnoDB の場合は primary キーがクラスタインデックスなので、インデックスフルスキャンとフルテーブルスキャンは同一視していいんだっけ?
-
- LIMIT で件数絞ったりする場合は例外アリ
SELECT * FROM foo
Handler_read_rnd_next しまくりなクエリ。 ORDER BY id [ASC|DESC]
にするとインデックススキャンになる
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | foo | ALL | NULL | NULL | NULL | NULL | 8 | NULL |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 9 |
+-----------------------+-------+
SELECT * FROM foo WHERE name = 'aaa' LIMIT 1
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | foo | ALL | NULL | NULL | NULL | NULL | 8 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 1 |
+-----------------------+-------+
- LIMIT で件数を絞っていると、指定された件数を見つけた段階で探索は終了する
- LIMIT 分にマッチする行が無ければフルテーブルスキャンになる
いずれにせよ効率は良くない
ORDER BY RAND()
を使うと出ます
SELECT * FROM foo ORDER BY rand()
+----+-------------+-------+------+---------------+------+---------+------+------+---------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+---------------------------------+
| 1 | SIMPLE | foo | ALL | NULL | NULL | NULL | NULL | 8 | Using temporary; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+------+---------------------------------+
今までの結果と比較すると圧倒的に効率悪いのが Handler_read_rnd, Handler_read_rnd_next の数値から分かる
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 8 |
| Handler_read_rnd_next | 18 |
+-----------------------+-------+