STRAIGHT_JOINをつかったSQLチューニング
こんにちは。PHPエンジニアの千葉です。
近所の金木犀が咲いたとおもったら、すぐに散ってしまって少しさびしいです。秋ですね…。
問題となった箇所
ゲソてんには、メッセージ機能というものがあり、
フレンド同士でメッセージのやりとりをしたり、
管理画面からメッセージを配信したりすることができます。
このメッセージ機能をお知らせとして使いたい、
という要望があり、管理画面から配信したものだけに絞り込んで
マイページに表示させてほしいという依頼がありました。
↓マイページをスクロールすると下の方にあります。
このSELECTクエリが今回の主役です。
こんなふうに変わっていきました
第1段階
既存のクエリをコピペして、それをベースに
要件を満たすように手を入れて作ったクエリ。filesortになっています。
mysql> explain SELECT
`message_relation_status`.*,
`message_status`.`to_status`
FROM
`message_status`
INNER JOIN
`message_relation_status`
ON (
`message_status`.`message_relation_status_id` = `message_relation_status`.`message_relation_status_id`
)
INNER JOIN
messages
ON message_status.message_id = messages.message_id
WHERE
(
`message_status`.`to_user_id` = yyy
AND `message_status`.`to_status` <= zzz
)
ORDER BY
`message_relation_status`.`message_relation_status_id` DESC
LIMIT 5 OFFSET 0;
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|-------------------------|--------|---------------------------------------------------------------------------------------|-----------------------|---------|-----------------------------------------------|------|--------------------------------------------------------|
| 1 | SIMPLE | message_status | range | PRIMARY,idx_message_status_01,idx_message_status_02,idx_message_status_03,index_test3 | idx_message_status_01 | 6 | NULL | 165 | Using index condition; Using temporary; Using filesort |
| 1 | SIMPLE | messages | eq_ref | PRIMARY,idx_messages_03 | PRIMARY | 8 | XXX.message_status.message_id | 1 | Using index |
| 1 | SIMPLE | message_relation_status | eq_ref | PRIMARY | PRIMARY | 8 | XXX.message_status.message_relation_status_id | 1 | NULL |
3 rows in set (0.00 sec)
第2段階
第1段階のクエリはfilesortになっていたので、DBチームに相談しました。
修正事項)
・WHERE句のmessage_status.to_user_idをmessage_relation_status.to_user_idに変更
結果)
駆動表がmessage_status からmessage_relation_statusに変わりました。
ソート済みの行からフェッチできるようになりfilesortが解消されました!
このクエリでリリースをしました。
mysql> explain SELECT
`message_relation_status`.*,
`message_status`.`to_status`
FROM
`message_status`
INNER JOIN
`message_relation_status`
ON (
`message_status`.`message_relation_status_id` = `message_relation_status`.`message_relation_status_id`
)
INNER JOIN
messages
ON message_status.message_id = messages.message_id
WHERE
(
`message_relation_status`.`to_user_id` = yyy
AND `message_status`.`to_status` <= zzz
)
ORDER BY
`message_relation_status`.`message_relation_status_id` DESC
LIMIT 5 OFFSET 0;
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|-------------------------|--------|-----------------------------------------------------------------|--------------------------------|---------|--------------------------------------------------------|------|-------------|
| 1 | SIMPLE | message_relation_status | ref | PRIMARY,idx_message_relation_status_02,idx_test2 | idx_message_relation_status_02 | 4 | const | 196 | Using where |
| 1 | SIMPLE | message_status | eq_ref | PRIMARY,idx_message_status_02,idx_message_status_03,index_test3 | PRIMARY | 8 | XXX.message_relation_status.message_relation_status_id | 1 | Using where |
| 1 | SIMPLE | messages | eq_ref | PRIMARY,idx_messages_03 | PRIMARY | 8 | XXX.message_status.message_id | 1 | Using index |
3 rows in set (0.00 sec)
第3段階
第1段階/第2段階のクエリはテスト環境で確認しましたが、
本番でスロークエリとして流れてくるようになった模様です。
EXPLAIN結果を見ると第2段階のときと変わっていますね…。。
mysql> explain SELECT
`message_relation_status`.*,
`message_status`.`to_status`
FROM
`message_status`
INNER JOIN
`message_relation_status`
ON (
`message_status`.`message_relation_status_id` = `message_relation_status`.`message_relation_status_id`
)
INNER JOIN
`messages`
ON (
`messages`.`message_id` = `message_status`.`message_id`
)
WHERE
`message_relation_status`.`to_user_id` = yyy
AND `message_status`.`to_status` <= zzz
ORDER BY
`message_relation_status`.`message_relation_status_id` DESC
LIMIT 5 OFFSET 0;
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|-------------------------|--------|-----------------------------------------------------|-----------------------|---------|-----------------------------------------------|------|----------------------------------------------|
| 1 | SIMPLE | messages | index | PRIMARY | idx_messages_01 | 6 | NULL | 2253 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | message_status | ref | PRIMARY,idx_message_status_02,idx_message_status_03 | idx_message_status_02 | 8 | XXX.messages.message_id | 2 | Using where; Using index |
| 1 | SIMPLE | message_relation_status | eq_ref | PRIMARY,idx_message_relation_status_02 | PRIMARY | 8 | XXX.message_status.message_relation_status_id | 1 | Using where |
3 rows in set (0.02 sec)
修正事項)
・FROM句のテーブルをmessage_status→message_relation_statusに変更
・message_statusとのJOINをINNER JOINからSTRAIGHT_JOINに変更
…STRAIGHT_JOINって初めて聞きました(。。)
STRAIGHT_JOIN は、左側のテーブルが常に右側のテーブルの前に読み取られる点を除き、JOIN と同じです。
https://dev.mysql.com/doc/refman/5.6/ja/join.html
結果)
ORDER BYのカラムがmessage_relation_statusのPKなので
message_relation_statusを駆動表にできれば最初からソート済になっています。
これを狙うためSTRAIGHT_JOINを使って、常に左側のテーブル(message_relation_status)が
先に読まれるようにすることで、第2段階のときと同じ実行計画になりました…!!
mysql> explain SELECT
`message_relation_status`.*,
`message_status`.`to_status`
FROM
`message_relation_status`
STRAIGHT_JOIN
`message_status` ON (`message_status`.`message_relation_status_id` = `message_relation_status`.`message_relation_status_id`)
INNER JOIN
`messages` ON (`messages`.`message_id` = `message_status`.`message_id`)
WHERE
`message_relation_status`.`to_user_id` = yyy
AND `message_status`.`to_status` <= zzz
ORDER BY `message_relation_status`.`message_relation_status_id` DESC
LIMIT 5 OFFSET 0;
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|-------------------------|--------|-----------------------------------------------------|--------------------------------|---------|--------------------------------------------------------|------|-------------|
| 1 | SIMPLE | message_relation_status | ref | PRIMARY,idx_message_relation_status_02 | idx_message_relation_status_02 | 4 | const | 4020 | Using where |
| 1 | SIMPLE | message_status | eq_ref | PRIMARY,idx_message_status_02,idx_message_status_03 | PRIMARY | 8 | XXX.message_relation_status.message_relation_status_id | 1 | Using where |
| 1 | SIMPLE | messages | eq_ref | PRIMARY | PRIMARY | 8 | XXX.message_status.message_id | 1 | Using index |
3 rows in set (0.01 sec)
参考)データ件数
テスト環境と本番環境でmessage_relation_statusのデータ件数が大きく異なったため、
テスト環境でのEXPLAIN結果と違う結果となり、リリース後にスロークエリとして流れてくるようになっていました。
messages | message_relation_status | |
---|---|---|
テスト環境 | 405 | 22,984 |
本番環境 | 2,281 | 44,747,015 |