はじめに
この記事はUdon Advent Calendar 2025 - Adventarの16日目の記事です。
クエリが重いとき
MySQLを使っていて、データベース内のレコードをSELECTするとき、JOINを使ってテーブルとテーブルを紐付けたり、WHEREを使って複雑な条件を設定したり、といった感じのクエリを作成すると思います。
ですが、いざ実行してみると、数十分かかっても実行が終わらない、ということがあります。
テーブルの規模にもよりますが、基本的に「規模の小さいテーブルではないとまともに動作しない」クエリはよろしくないので、クエリの改善が必要になってきます。
クエリの改善方法はいくつかありますが、改善のためにはクエリのどの部分がボトルネックになっているのかを確認することが近道です。
今回は、クエリを解析し、クエリの実行計画に関するデータを表示してくれるEXPLAINについて説明したいと思います。
EXPLAIN機能
EXPLAINは、打ち込んだクエリが実行されるとき、どのような実行計画が立てられているのかを表示してくれます。
まずはやってみましょう。あるテーブルに対して、以下のクエリを実行することを考えましょう。
SELECT * FROM table_1 WHERE created_at >= 2025-10-01;
クエリ自体は特定期間のレコードを全件表示するだけの単純なものです。このクエリの実行計画を確認してみましょう。
確認のためには、以下のクエリを実行します。実行計画を調べたいクエリの先頭にEXPLAINとつけたものとなります。
EXPLAIN SELECT * FROM table_1 WHERE created_at >= 2025-10-01;
これを実行すると以下のようなテーブルが出てきます。
mysql> EXPLAIN SELECT * FROM table_1 WHERE created_at >= 2025-10-01;
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | table_1 | NULL | ALL | NULL | NULL | NULL | NULL | 1877 | 33.33 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 2 warnings (0.02 sec)
何やらたくさん列がありますね。
列の種類
上記テーブルの列はそれぞれ実行計画の詳細情報を表しています。
ここからは、それぞれの列について見ていきましょう。
id
SELECTの実行順を表しています。
select_type
SELECTの種類で、例えば以下のようなものがあります。
-
SIMPLE:UNIONやサブクエリの使用をしないもの -
SUBQUERY:サブクエリ内の最初のSELECT
table
クエリがアクセスしようとしているテーブルの名前です。
partitions
クエリによってレコードが照合されるパーティションがリストアップされるそうです。
type
クエリがどのようにテーブルにアクセスするかを示す重要なパラメータです。これをチェックすることで、クエリのパフォーマンスをチェックできます。
以下、遅い方から順に書いていきます。
-
ALL- 全レコードをスキャンしている
- インデックスが全く使用されていない
- 根本的に見直しが必要
-
index- 全インデックスをスキャンしている
- まだまだ低速なので見直しが必要
-
range- インデックスを用いた範囲検索が行われる
- 比較演算子を用いたクエリで使われる
- まだまだ遅い場合は範囲や絞り込み条件の見直しが必要
-
ref- ユニークではないインデックスでの等価検索が行われる
-
eq_ref- テーブルの結合時に主キーやユニークキーが用いられることを示す
-
JOINの際にこれが出ていることが理想
-
const- 主キーやユニークキーを用いたアクセスが行われる
- これが出ていることが理想
- 検索対象が1つに絞り込めている
possible_keys
後述のkeyの候補です。
key
オプティマイザにより選択されたインデックスであり、実行時間が最小になるような選択がなされています。
ここに表示されているものが想定と異なる場合はインデックスの見直しが必要となります。
PRIMARYと表示されていると、主キーをインデックスとして利用していることとなります。この場合は高速です。
NULLの場合はインデックスが見つからないためテーブル全体をスキャンしてしまうので、低速となります。
key_len
選択されたインデックスの長さを表示しています。
多くの場合、短い方が高速となります。
ref
keyと比較されている値・カラムが表示されています。
rows
クエリを実行する際に読み込みが必要なレコード数の見積もり値で、重要なパラメータです。
当然、この値が小さい程読み込む必要のあるレコード数が減るのでクエリが高速化します。
この値が大きいと、十分にレコード数を絞り込めていないということになるので、検索条件などの見直しが必要になります。
filtered
条件によってフィルタリングされたレコード数が取得レコード全体の何割なのかを推定したパラメータです。
一見値が小さい方が良いように見えますが、これは誤りです。
MySQLは実行計画を立てたあと、以下の順番でクエリを処理します。
- データの取得(適切なインデックスがある場合、ここで使用する)
- フィルタリング(
WHEREやJOINを用いてデータを絞り込む)
filteredは、この2.で絞り込まれたレコードの数の1.で取得したレコード数に占める割合なのです。
そのため、100に近い方が余分なレコードを読んでいないことになります。これはインデックスが適切に使用できているということになるので、高速なクエリとなります。
Extra
オプティマイザがクエリ実行の際に採用する戦略を表します。クエリの速さに直結する重要な部分です。
以下、遅い順に書いていきます。
-
Using filesort-
ORDER BYにおいてインデックスが使用できず、クイックソートが行われようとしている - レコード数が多いとパフォーマンスが悪化する
- インデックスが設定されているカラムで
ORDER BYをすると改善する
-
-
Using temporary- クエリの解決のために一時テーブルが利用されようとしている
-
GROUP BYやORDER BY、DISTINCTなど中間結果を保持する必要がある場合に出現する - オーバーヘッドが大きくなるため、パフォーマンスが悪化する
-
Using Where- 検索の際に
WHEREが用いられている
- 検索の際に
-
Using index condition- 複合インデックスが効率的に用いられている
- 対象レコード数が多いとパフォーマンスが悪化することもある
-
Using index- 検索の際にインデックスが用いられていて、高速である
特に重要な列
上で説明した情報はどれも大切ですが、特に大切なものは以下の5つでしょう。
typekeyrowsfilteredExtra
これらの列の値がどうなっていれば高速なのか、逆にどうなっていると低速になるのかを押さえておくと、現在のクエリの問題点がわかると思います。
複数テーブルをJOINした場合
先程は1つのテーブルに対するクエリの実行計画を確認してみましたが、次は複数テーブルに対するクエリの実行計画を見てみましょう。
mysql> EXPLAIN SELECT * FROM (SELECT * FROM table_1) AS t1 JOIN (SELECT * FROM table_2) AS t2 ON t1.row1 = t2.row2;
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| 1 | SIMPLE | table_1 | NULL | ALL | NULL | NULL | NULL | NULL | 1877 | 100.00 | NULL |
| 1 | SIMPLE | table_2 | NULL | ALL | NULL | NULL | NULL | NULL | 5617 | 10.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.02 sec)
このように、テーブルごとに実行計画が現れます。
おわりに
大規模なデータベースで集計などを行う場合、効率の良いSQL文を書かないと実行がなかなか終わらなかったり、そもそも再現性がなくなってしまったりなどということが起きます。
そういった時は今回紹介したEXPLAINを使ってみて、どこが処理のボトルネックなのかを確認してみましょう。
JOIN文が優先的に実行されるなどのルールを使って、できるだけテーブルの規模を小さくしてから集計を行うなどの工夫をすることで、こういったボトルネックを解消することが効率的なクエリ実行のポイントとなるでしょう。
それではまた、明日の記事でお会いしましょう!
参考文献
SQL実行計画の疑問解決には「とりあえずEXPLAIN」しよう | Think IT(シンクイット)
MySQL の EXPLAIN を読むときの勘所 #パフォーマンスチューニング - Qiita
EXPLAIN実行結果の読み方について解説 - MySQL #DB - Qiita
MySQL :: MySQL 8.0 リファレンスマニュアル :: 24.3.5 パーティションに関する情報を取得する