実は2年くらい前まで使ったことありませんでした…。
…というわけでマニュアル読みましょう!
EXPLAIN は、クエリー実行計画 (つまりMySQLがクエリーをどのように実行するかの説明) を取得するために使用されます。
とありますが、構文とかいろいろ小難しいことが
書いてあってよくわかりません。
…どうやって使えばよいのか?
「mysql explain」でググると沢山解説ページが出てきます。
このへん見れば大体分かります。
…EXPLAINの要点をまとめると
…じゃあ実際にやってみよう!
あるテーブルを全部selectしてみる。。
mysql> explain select * from purchase; +----+-------------+----------+------+---------------+------+---------+------+-----------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+-----------+-------+ | 1 | SIMPLE | purchase | ALL | NULL | NULL | NULL | NULL | 180298799 | NULL | +----+-------------+----------+------+---------------+------+---------+------+-----------+-------+
‥typeとはなんぞや?
…インデックスで検索した場合のEXPLAIN
プライマリキー
mysql> EXPLAIN SELECT * FROM purchase WHERE purchase_id = 310000000; +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+ | 1 | SIMPLE | purchase | const | PRIMARY | PRIMARY | 8 | const | 1 | NULL | +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
mysql> EXPLAIN SELECT * FROM purchase WHERE basket_service_type = 'admin'; +----+-------------+----------+------+---------------+--------------+---------+-------+---------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+--------------+---------+-------+---------+-----------------------+ | 1 | SIMPLE | purchase | ref | basket_serv. | basket_serv. | 258 | const | 3068718 | Using index condition | +----+-------------+----------+------+---------------+--------------+---------+-------+---------+-----------------------+
mysql> explain select * from purchase where grant_id is null; +----+-------------+----------+------+---------------+--------------+---------+-------+----------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+--------------+---------+-------+----------+-----------------------+ | 1 | SIMPLE | purchase | ref | grant_id | grant_id | 9 | const | 88052842 | Using index condition | +----+-------------+----------+------+---------------+--------------+---------+-------+----------+-----------------------+
…ではインデックスが使われないパターンは?
mysql> explain select * from purchase where tax = 0; +----+-------------+----------+------+---------------+------+---------+------+-----------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+-----------+-------------+ | 1 | SIMPLE | purchase | ALL | NULL | NULL | NULL | NULL | 180298726 | Using where | +----+-------------+----------+------+---------------+------+---------+------+-----------+-------------+
transaction_idにインデックスは貼ってあるけれど‥
mysql> explain select * from settlement where transaction_id = 123456789; +----+-------------+------------+------+----------------+------+---------+------+-----------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+------+----------------+------+---------+------+-----------+-------------+ | 1 | SIMPLE | settlement | ALL | transaction_id | NULL | NULL | NULL | 181576359 | Using where | +----+-------------+------------+------+----------------+------+---------+------+-----------+-------------+
…正しい書き方は?
mysql> explain select * from settlement where transaction_id = '123456789'; +----+-------------+------------+------+----------------+----------------+---------+-------+------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+------+----------------+----------------+---------+-------+------+-----------------------+ | 1 | SIMPLE | settlement | ref | transaction_id | transaction_id | 82 | const | 1 | Using index condition | +----+-------------+------------+------+----------------+----------------+---------+-------+------+-----------------------+
昨日までのデータをちょっと見たい‥
EXPLAIN SELECT * FROM settlement WHERE TIMEDIFF(now(), settlement_date) < 3600 * 24; +----+-------------+------------+------+---------------+------+---------+------+-----------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+------+---------------+------+---------+------+-----------+-------------+ | 1 | SIMPLE | settlement | ALL | NULL | NULL | NULL | NULL | 181738042 | Using where | +----+-------------+------------+------+---------------+------+---------+------+-----------+-------------+
EXPLAIN SELECT * FROM settlement WHERE settlement_date > now() - interval 1 day; +----+-------------+------------+-------+----------------+----------------+---------+------+--------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+-------+----------------+----------------+---------+------+--------+-----------------------+ | 1 | SIMPLE | settlement | range | settlement_date | settlement_date | 5 | NULL | 338668 | Using index condition | +----+-------------+------------+-------+----------------+----------------+---------+------+--------+-----------------------+
タウンページで例えると‥
都道府県名 → 探すのが大変!
カーディナリティが高いものが優先
やりたいこと
会員IDとステータスにはインデックスが貼ってある
インデックスはどう使われる‥?
会員IDで絞る?
会員IDのインデックスが使われるはず‥
mysql> explain select * from purchase where status = 'success' and member_id = 'お得意様のID'; +----+-------------+----------+------+---------------------------+--------------+---------+-------+-------+------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------------------+--------------+---------+-------+-------+------------------------------------+ | 1 | SIMPLE | purchase | ref | member_id, status | member_id | 48 | const | 46552 | Using index condition; Using where | +----+-------------+----------+------+---------------------------+--------------+---------+-------+-------+------------------------------------+
処理中データを探すときは ステータス < 会員ID
‥今度はステータスのインデックスが使われるはず
mysql> explain select * from purchase where status = 'processing' and member_id = 'お得意様'; +----+-------------+----------+------+---------------------------+--------------+---------+-------+------+------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------------------+--------------+---------+-------+------+------------------------------------+ | 1 | SIMPLE | purchase | ref | member_id,status | status | 1 | const | 16 | Using index condition; Using where | +----+-------------+----------+------+---------------------------+--------------+---------+-------+------+------------------------------------+
mysql> explain select * from purchase where service_id = 0 or basket_service_type = 'admin'; +----+-------------+----------+-------------+---------------------------+---------------------------+---------+------+---------+-----------------------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------------+---------------------------+---------------------------+---------+------+---------+-----------------------------------------------------+ | . | . | purchase | index_merge | basket_service_type,service_id | service_id,basket_service_type | 4,258 | NULL | 3114932 | Using +----+-------------+----------+-------------+---------------------------+---------------------------+---------+------+---------+-----------------------------------------------------+
インデックスが効くものとそうでないもの
limit
‥クエリの組み立てを通して確認!
テーブル概要
SELECT COALESCE(p.purchase_id, ch.charge_id) as 注文ID ,COALESCE(p.amount, ch.sales_price) AS 金額 FROM cancel as c LEFT JOIN purchase AS p ON (c.order_id = p.purchase_id) LEFT JOIN charge AS ch ON (c.order_id = ch.charge_id) WHERE (p.member_id = 'ある会員' OR ch.member_id = 'ある会員') AND c.status = 'success'
+----+-------------+-------+--------+---------------+------------+---------+---------------------+--------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+---------------+------------+---------+---------------------+--------+-----------------------+ | 1 | SIMPLE | c | ref | status | status | 1 | const | 302929 | Using index condition | | 1 | SIMPLE | p | eq_ref | PRIMARY | PRIMARY | 8 | purchase.c.order_id | 1 | NULL | | 1 | SIMPLE | ch | eq_ref | PRIMARY | PRIMARY | 8 | purchase.c.order_id | 1 | Using where | +----+-------------+-------+--------+---------------+------------+---------+---------------------+--------+-----------------------+
SELECT COALESCE(p.purchase_id, ch.charge_id) as 注文ID ,COALESCE(p.amount, ch.sales_price) AS 金額 FROM cancel as c LEFT JOIN (SELECT * FROM purchase WHERE member_id = '会員ID') AS p ON (c.order_id = p.purchase_id) LEFT JOIN (SELECT * FROM charge WHERE member_id = '会員ID') AS ch ON (c.order_id = ch.charge_id) AND c.status = 'success'
+----+-------------+------------+------+---------------+--------------+---------+---------------------+--------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+------+---------------+--------------+---------+---------------------+--------+-----------------------+ | 1 | PRIMARY | c | ALL | NULL | NULL | NULL | NULL | 586057 | NULL | | 1 | PRIMARY | <derived2> | ref | <auto_key0> | <auto_key0> | 8 | purchase.c.order_id | 10 | NULL | | 1 | PRIMARY | <derived3> | ref | <auto_key0> | <auto_key0> | 8 | purchase.c.order_id | 10 | Using where | | 3 | DERIVED | charge | ref | member_id | member_id | 48 | const | 923 | Using index condition | | 2 | DERIVED | purchase | ref | member_id | member_id | 48 | const | 1596 | Using index condition | +----+-------------+------------+------+---------------+--------------+---------+---------------------+--------+-----------------------+
ダメな例
SELECT order_id as 注文ID ,price AS 金額 FROM ( SELECT /* 必要なカラム */ FROM purchase as p inner join cancel as c ON p.purchase_id = c.cancel_id UNION ALL SELECT /* 必要なカラム */ FROM charge as ch inner join cancel as c ON ch.charge_id = c.cancel_id ) AS tmp WHERE tmp.member_id = 'ある会員ID' and tmp.status = 'success';
+----+--------------+------------+--------+-------------------+-------------+---------+----------------------+--------+-----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------+------------+--------+-------------------+-------------+---------+----------------------+--------+-----------------+ | 1 | PRIMARY | <derived2> | ref | <auto_key0> | <auto_key0> | 90 | const,const | 10 | Using where | | 2 | DERIVED | c | index | PRIMARY,cancel_id | status | 1 | NULL | 586057 | Using index | | 2 | DERIVED | p | eq_ref | PRIMARY | PRIMARY | 8 | purchase.c.cancel_id | 1 | Using where | | 3 | UNION | c | index | PRIMARY,cancel_id | status | 1 | NULL | 586057 | Using index | | 3 | UNION | ch | eq_ref | PRIMARY | PRIMARY | 8 | purchase.c.cancel_id | 1 | Using where | | NULL | UNION RESULT | <union2,3> | ALL | NULL | NULL | NULL | NULL | NULL | Using temporary | +----+--------------+------------+--------+-------------------+-------------+---------+----------------------+--------+-----------------+ 6 rows in set (0.00 sec)
SELECT order_id as 注文ID ,price AS 金額 FROM ( SELECT purchase_id as order_id , p.amount as price , member_id , c.status FROM purchase as p INNER JOIN cancel as c ON p.purchase_id = c.cancel_id WHERE p.member_id = 'ある会員' and c.status = 'success' UNION ALL SELECT charge_id as order_id, member_id, sales_price as price, c.status FROM charge as ch inner join cancel as c ON ch.charge_id = c.cancel_id WHERE ch.member_id = 'ある会員' and c.status = 'success' ) AS tmp
+----+--------------+------------+--------+------------------------------+--------------+---------+------------------------+------+-----------------------+ | id | select_type | table | type | possible_keys | key | .. | rows | Extra | +----+--------------+------------+--------+------------------------------+--------------+---------+------------------------+------+-----------------------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | .. | 2519 | NULL | | 2 | DERIVED | p | ref | PRIMARY,member_id | member_id | .. | 1596 | Using index condition | | 2 | DERIVED | c | eq_ref | PRIMARY,cancel_id,status | PRIMARY | .. | 1 | Using where | | 3 | UNION | ch | ref | PRIMARY,member_id | member_id | .. | 923 | Using index condition | | 3 | UNION | c | eq_ref | PRIMARY,cancel_id,status | PRIMARY | .. | 1 | Using where | | NULL | UNION RESULT | <union2,3> | ALL | NULL | NULL | .. | NULL | Using temporary | +----+--------------+------------+--------+------------------------------+--------------+---------+------------------------+------+-----------------------+
架空の機能を実装してみる
+--------------+------------+--------------+--------+ | member_id | クレカ合計 | DMMポイント合計 | 合計 | +--------------+------------+--------------+--------+ | 会員X| 100575 | 0 | 100575 | | 会員C| 79904 | 7264 | 87168 | | 会員D| 82684 | 0 | 82684 | | 会員P| 80460 | 0 | 80460 | | 会員S| 80127 | 0 | 80127 | +--------------+------------+--------------+--------+ 5 rows in set (0.24 sec)
SELECT tmp.* ,credit_sum + dmmpoint_sum AS sum # 合計 FROM ( SELECT member_id, SUM( CASE WHEN pay_type_main = 'credit' THEN amount ELSE 0 END ) AS credit_sum, # クレカ SUM( CASE WHEN pay_type_main = 'dmmpoint' THEN amount ELSE 0 END ) AS dmmpoint_sum # DMMポイント FROM purchase WHERE purchase_id between 昨日のID and 今日のID GROUP BY member_id ) AS tmp ORDER BY sum DESC LIMIT 5
+----+-------------+------------+-------+----------------------+---------+---------+------+--------+----------------------------------------------+ | id | select_type | table | type | possible_keys | key | .. | rows | Extra | +----+-------------+------------+-------+----------------------+---------+---------+------+--------+----------------------------------------------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | .. | 118310 | Using filesort | | 2 | DERIVED | purchase | range | PRIMARY,member_id | PRIMARY | .. | 118310 | Using where; Using temporary; Using filesort | +----+-------------+------------+-------+----------------------+---------+---------+------+--------+----------------------------------------------+
プライマリインデックスでソート
mysql> explain select * from purchase order by purchase_id desc limit 10; +----+-------------+----------+-------+---------------+---------+---------+------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+---------+---------+------+------+-------+ | 1 | SIMPLE | purchase | index | NULL | PRIMARY | 8 | NULL | 10 | NULL | +----+-------------+----------+-------+---------------+---------+---------+------+------+-------+
mysql> explain select * from purchase order by purchase_date desc limit 10; +----+-------------+----------+-------+---------------+--------------+---------+------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+--------------+---------+------+------+-------+ | 1 | SIMPLE | purchase | index | NULL | purchase_ix4 | 6 | NULL | 10 | NULL | +----+-------------+----------+-------+---------------+--------------+---------+------+------+-------+ 1 row in set (0.00 sec)
mysql> explain select * from purchase order by amount desc limit 10; +----+-------------+----------+------+---------------+------+---------+------+-----------+----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+-----------+----------------+ | 1 | SIMPLE | purchase | ALL | NULL | NULL | NULL | NULL | 175771186 | Using filesort | +----+-------------+----------+------+---------------+------+---------+------+-----------+----------------+
mysql> explain select * from purchase where member_id = 'ある会員ID' order by purchase_id desc limit 10; +----+-------------+----------+------+---------------+--------------+---------+-------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+--------------+---------+-------+-------+-------------+ | 1 | SIMPLE | purchase | ref | member_id | member_id | 48 | const | 44448 | Using where | +----+-------------+----------+------+---------------+--------------+---------+-------+-------+-------------+ selectの結果 10 rows in set (0.00 sec)
セカンダリインデックスでソート
explain select * from purchase force index(member_id) where member_id = 'ある会員' order by purchase_date desc limit 10; +----+-------------+----------+-------+---------------+--------------+---------+------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+--------------+---------+------+-------+-------------+ | 1 | SIMPLE | purchase | index | member_id | member_id | 6 | NULL | 44448 | Using where | +----+-------------+----------+-------+---------------+--------------+---------+------+-------+-------------+ selectの結果 10 rows in set (0.17 sec)
セカンダリインデックスでソート
mysql> explain select * from purchase where member_id = 'ある会員' order by purchase_date desc limit 10; +----+-------------+----------+-------+---------------+--------------+---------+------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+--------------+---------+------+-------+-------------+ | 1 | SIMPLE | purchase | index | member_id | purchase_date | 6 | NULL | 39545 | Using where | +----+-------------+----------+-------+---------------+--------------+---------+------+-------+-------------+ selectの結果 10 rows in set (1.41 sec)
一見早そうに見えるけど…
mysql> explain select * from purchase where order_date < '2004-01-01' order by purchase_date desc limit 1; +----+-------------+----------+-------+---------------+--------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+--------------+---------+------+------+-------------+ | 1 | SIMPLE | purchase | index | order_date | purchase_date | 6 | NULL | 2 | Using where | +----+-------------+--------+-------+---------------+--------------+---------+------+------+-------------+ 1 row in set (0.00 sec)
SELECT number FROM credit_log as cl INNER JOIN purchase as p on cl.member_id = p.member_id WHERE p.purchase_id = 310000000 LIMIT 1
+----+-------------+-------+-------+----------------------+---------+---------+-------+----------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+----------------------+---------+---------+-------+----------+-------------+ | 1 | SIMPLE | p | const | PRIMARY,purchase_ix1 | PRIMARY | 8 | const | 1 | NULL | | 1 | SIMPLE | cl | ALL | NULL | NULL | NULL | NULL | 13566662 | Using where | +----+-------------+-------+-------+----------------------+---------+---------+-------+----------+-------------+
SELECT number FROM credit_log as cl INNER JOIN purchase as p on cl.member_id = CONVERT(p.member_id USING ujis) WHERE p.purchase_id = 310000000 LIMIT 1
+----+-------------+-------+-------+---------------+-----------+---------+-------+------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+-----------+---------+-------+------+-----------------------+ | 1 | SIMPLE | p | const | PRIMARY | PRIMARY | 8 | const | 1 | NULL | | 1 | SIMPLE | cl | ref | member_id | member_id | 38 | const | 1 | Using index condition | +----+-------------+-------+-------+---------------+-----------+---------+-------+------+-----------------------+
select c.* # 返金情報 reason as 返金理由 FROM ( select 0 AS reason_id,'理由' AS reason union all select 1, '障害' union all select 2, '検証' union all select 3, 'テスト' union all select 4, '間違い' union all select 5, '品切れ' union all select 6, '乱丁・落丁' ‥ # 1万行くらい union all select 10000, 'その他' ) as tmp INNER JOIN cancel as c ON c.reason_id = tmp.reason_id
# CSVにデータエクスポート set names ujis; select * from テーブル into outfile a.csv # CSVのデータを変換 perl -p -e 's/\x8f\xa1\xc1/\xa1\xc1/g' CSVファイル > 出力先 # CSVデータをインポート set names utf8mb4; LOAD DATA INFILE ... # csv読み込み
explain update cancel_detail as cd inner join cancel as c on c.cancel_id = cd.cancel_id inner join purchase as p on c.order_id = p.purchase_id inner join purchase_detail as pd on cd.purchase_detail_id = pd.purchase_detail_id SET cd.amount = -((pd.sales_price + pd.unit_tax) * pd.quantity) where p.basket_service_type = 'admin' # 管理画面から取り消されたものを対象
でも本番のマスターDBでやるとテーブルロックかかったりして後続のトランザクションがタイムアウトで死んだりするので できるだけプライマリキーで1つずつやるようにしましょう!
http://explainextended.com/2010/01/30/aggregates-subqueries-vs-group-by/
SQLを速くするぞ!
MySQL 5.6における大量データロード時の考慮点
大きめのテーブルにカラムやインデックスを追加する際の注意
MySQL Server System Variables
http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_interactive_timeout
innodbチューニング
InnoDBの圧縮を使うときの運用
group by で集約していないカラムを使うと"適当に"選択される
http://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html
InnoDBに直接アクセス
http://labs.cybozu.co.jp/blog/kazuho/archives/2008/06/mysql_direct_access.php
MySQL 5.6における大量データロード時の考慮点