実は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における大量データロード時の考慮点