🕌

スロークエリのチューニングについて

2022/09/22に公開

ZENKIGEN「harutaka」開発チームの川俣です。私が開発に携わっている採用DXサービス「harutaka」で発生したスロークエリを改善した際のプロセスを書いていきます。

https://harutaka.jp/

SQLチューニングの手順

SQLチューニングは大まかに3つの手順で行っていきます。

1. スロークエリの検知

MySQLの設定ファイルからスロークエリログを出力するようにすることで検知できます。

弊社サービス「harutaka」ではDatadogによる監視をおこなっていて、レイテンシが閾値を超えるスロークエリが発生した場合Slackにアラートが飛ぶようになっています。

アラートから発生したスロークエリを確認します。
今回発生したスロークエリはこちらです。(テーブル名/カラム名は適宜変えています)

SELECT
    *
FROM
    hoges
WHERE
    cloumn_1 = 1
AND 
    cloumn_2 = 2;

2. 原因の調査

実行計画の確認

検知したスロークエリの原因を突き止めるため実行計画を確認します。
クエリの先頭にEXPLAINをつけて実行します。

EXPLAIN
SELECT
    *
FROM
    hoges
WHERE
    cloumn_1 = 1
AND 
    cloumn_2 = 2;

実行計画はこちら

id table select_type partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE hoges NULL ref index_hoges_on_column_1,index_hoges_on_column_2 index_hoges_on_column_2 1 const 100 100.0 Using where

実行計画で特に見なければいけないのはtypeの列です。
スロークエリの原因はインデックスであることが多いのでまずはここを確認します。

■ 代表的なtype

type 意味
const Primary keyもしくはユニークインデックスによるアクセス
eq_ref JOIN時にPrimary keyもしくはユニークインデックスを利用したアクセス
ref ユニークではないインデックスによる等価検索
range インデックスを用いた範囲検索
index フルインデックススキャン
ALL フルテーブルスキャン

スロークエリの場合typeがindexALLとなっていることが多いです。
この場合はインデックスが利用できていないので、where句で指定されているカラムにインデックスが作成されているか?などを調査して対応する必要があります。

しかし、今回の例ではrefとなっていてスロークエリの原因は見当たらないように思えます。

その場合はさらに調査するためテーブルのインデックス情報を見ていきましょう。

インデックス情報の確認

確認方法はこちら

SHOW INDEX FROM hoges;

インデックス情報はこちら

Table Non_unique Key_name Seq_in_index Cloumn_name Collation Cardinality Sub_part Packed Null index_type Comment index_comment
hoges 0 PRIMARY 1 id A 1000000 NULL NULL BTREE
hoges 0 index_hoges_on_column_1 1 column_1 A 500000 NULL NULL YES BTREE
hoges 0 index_hoges_on_column_2 1 column_2 A 5 NULL NULL YES BTREE

SQLチューニングの際はCardinalityに着目します。
Cardinalityとは、カラムに格納されているデータの種類がどのくらいあるのか(カラムの値の種類の絶対値)を指しています。
基本的にインデックスのCardinalityが高いほど絞り込みが高速化され、パフォーマンス改善につながります。

改めてインデックス情報を見ると、スロークエリでインデックスとして利用されているindex_hoges_on_column_2のCardinalityは5に対してもう一つのインデックスindex_hoges_on_column_1は500000となっていて、Cardinalityが低いインデックスが利用されていることがわかります。これがスロークエリの原因のようです。

なぜCardinalityが低いインデックスが利用されてしまうのかというと、MySQLのオプティマイザを理解する必要があります。

オプティマイザ

オプティマイザとは具体的なデータの取得処理(実行計画)をクエリ発行ごとに生成する機能です。

なぜ実行計画をクエリ発行ごとに生成するかというと、統計情報によって最適な実行計画が変わるためです。

MySQLのオプティマイザは、以下の統計情報を元に実行計画を作成します。
• テーブルに含まれる行数
• 各インデックスのサイズ(ページ数)
• 各インデックスのリーフページのサイズ(ページ数)
• 各インデックスのカーディナリティ(何種類の値が存在するか?)

例えば今回のクエリでfrom句に指定しているhogesテーブルのレコード数が1行しかない場合、インデックスを使わない実行計画の方が無駄なアクセス処理が無いので、インデックスを使わない時に比べ実行速度は早くなるでしょう。
一方レコード数が数万行もある場合は、インデックスを使った方が効率良く該当レコードを絞り込めるので、インデックス無しより早くなります。

つまりクエリが同じでも統計情報が変わればオプティマイザの作成する実行計画は変わり、パフォーマンスも大きく変わる可能性があります。
そしてオプティマイザの判断は間違うこともあり、必ずしも最適だとは限りません。
オプティマイザがより良い実行計画を作成できるように、SQLチューニングを行う必要があります。

3. スロークエリの改善

今回のスロークエリの原因はCardinalityが低いインデックスが利用されていたからでした。
これに対しての改善案として、既存インデックスでよりCardinalityが高いインデックスを利用させることにしました。

インデックスヒント

MySQLではオプティマイザに対してインデックスの選択を変更させるインデックスヒントがあります。
今回はFORCE INDEXを利用してクエリで利用するインデックスを指定します。

SELECT
    *
FROM
    hoges
FORCE INDEX (index_hoges_on_column_1)
WHERE
    cloumn_1 = 1
AND 
    cloumn_2 = 2;

実行計画を見ると

id table select_type partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE hoges NULL ref index_hoges_on_column_1,index_hoges_on_column_2 index_hoges_on_column_1 1 const 100 100.0 Using where

インデックスにFORCE INDEXで指定したindex_hoges_on_column_1が利用されていますね。

このSQLで実行時間を計測するとスロークエリの時と比べて数百倍短縮されていました。

インデックスヒントは便利な一方、動的にインデックスを選択してくれるオプティマイザの利点を消してしまう部分もあるので使い方には注意が必要です。
where句が動的に変わるようなクエリの場合はテストをしっかり行い、逆にパフォーマンスが低下することがないか確認しましょう。

まとめ

今回はスロークエリ改善のプロセスをご紹介しました。参考頂けると嬉しいです。

お知らせ

少しでも弊社や harutaka 、revii に興味を持っていただいたという方は、お気軽にご連絡頂けると幸いです!カジュアルにお話という形でも、副業したいという形でも大歓迎です。

https://recruit.zenkigen.co.jp/career

参照

https://dev.mysql.com/doc/refman/5.6/ja/explain-output.html#explain-join-types

http://nippondanji.blogspot.com/2009/03/mysqlexplain.html

https://qiita.com/soyanchu/items/034be19a2e3cb87b2efb

https://dev.mysql.com/doc/refman/5.6/ja/index-hints.html

https://qiita.com/towtow/items/db397d39416adf9770aa

https://downloads.mysql.com/presentations/20151208_02_MySQL_Tuning_for_Beginners.pdf

Discussion