メインコンテンツへスキップ

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
遅いクエリは、ほぼ常にいくつかのよくある原因に行き着きます。インデックスの欠如、誤ったインデックス、インデックスの使用を妨げる書き方のクエリ、インデックス付きの列に適用された関数、N+1 パターン、または同時に実行されている他のクエリとの競合です。そのほとんどは、2 つのコマンドで診断できます: EXPLAIN とスロークエリログです。そこから始めてください。

ステップ 1: EXPLAIN を読む

調べたいクエリの前に EXPLAIN を付けて実行します。
次のような出力が得られます。
診断の仕事の大半は 5 つの列が担います。 possible_keys — このクエリのために MySQL が検討したインデックスです。NULL は、WHERE 句の列に関連するインデックスが存在しないことを意味します。 key — MySQL が実際に選んだインデックスです。possible_keys に候補があるのに key が NULL なら、MySQL はフルテーブルスキャンのほうが安いと判断したということです。通常は、統計が古いか、オプティマイザがインデックスを飛ばすほどテーブルが小さいかのどちらかの兆候です。 type — MySQL が使ったアクセス方法です。これが最も重要な列です。ALL はフルテーブルスキャンを意味します。MySQL は一致する行を見つけるためにすべての行を読みました。800,000 行のテーブルでは、1 行しか返さないかもしれないクエリのために 800,000 回の行の読み取りです。ref または eq_ref は、MySQL がインデックス検索を使ったことを意味し、これが求めるものです。 key — MySQL が選んだインデックスです。NULL はインデックスが使われなかったことを意味します(上の possible_keys を参照)。 rows — MySQL が調べる行数の見積もりです。返ってくると期待する行数に比べて大きな数値は、何かがおかしい兆候です。 Extra — 補足情報です。Using filesort は、ORDER BY をカバーするインデックスがなかったため、MySQL が結果セットをメモリまたはディスク上でソートしなければならなかったことを意味します。Using temporary は、MySQL が一時テーブルを作ったことを意味し、特定の GROUP BY と DISTINCT のクエリで起こります。どちらも大きなテーブルでは警告のサインです。 見積もりを読んだら、EXPLAIN ANALYZE を使って、実行時に実際に何が起きたかを確認します。
EXPLAIN ANALYZE はクエリを実行し、見積もりと並べて実際の行数と時間を報告します。見積もりが 800,000 行で実際も 800,000 行なら、見積もりは正しく、スキャンは実際に起きています。実際の値がはるかに低いなら、オプティマイザは古い統計に基づいて不適切な選択をしたのかもしれません。

ステップ 2: 最もよくある原因

インデックスの欠如

上の例のクエリは、customer_id にインデックスがないため、orders テーブル全体をスキャンします。修正は単純です。
インデックスを追加した後、EXPLAIN を再実行します。type 列は ALL から ref に変わり、key には新しいインデックス名が表示されるはずです。

誤ったインデックス

MySQL は、最良ではないインデックスを選ぶことがあります。EXPLAIN はどれを選んだかを教えてくれます。別のインデックスのほうが良い結果になるかをテストするには、FORCE INDEX を使います。
強制したインデックスのほうが rows の見積もりが低いなら、元のインデックスの統計が古くなっていないか検討してください。ANALYZE TABLE orders がそれを再計算します。MySQL が一貫して誤ったインデックスを選ぶなら、もう役に立たないインデックスがあるのかもしれません。それを削除すると、検討対象から外れます。

先頭列の問題

(customer_id, status, created_at) の複合インデックスは、クエリが customer_id で、または customer_id と status の両方でフィルタリングするときに効率的です。クエリが status だけ、または created_at だけでフィルタリングする場合、インデックスはまず customer_id でソートされているため、効率的には使えません。
status だけでフィルタリングする必要があるなら、status に別のインデックスを追加します。

SELECT *

必要な列だけを指定することは、単なる良いスタイルではなく、パフォーマンスに影響します。MySQL がインデックス(カバリングインデックス)だけでクエリを完全に満たせるとき、実際のテーブルの行には触れません。SELECT * はそれを壊します。インデックスにない列を取得するために、一致する各行をテーブルで検索することを MySQL に強いるからです。

インデックス付きの列に対する関数

インデックス付きの列を関数で包むと、通常はインデックスが使えなくなります。インデックスは計算された年ではなく生のタイムスタンプを格納しているため、MySQL は「YEAR() が 2024 を返す created_at を持つ行はどれか」という問いに B-tree インデックスで答えられません。
同じ問題は、LOWER()、DATE()、列に対する算術演算、および比較の前に値を変換するその他すべてに当てはまります。

N+1 クエリ

問題が 1 つの遅いクエリではなく、何百もの速いクエリであることがあります。よくあるパターンは、注文の一覧を取得し、次に各注文について顧客名を得るために別のクエリを実行するというものです。200 件の注文なら、201 回のクエリです。1 回ずつは速くても、合わせると数秒になります。 修正は、JOIN で 1 つのクエリにデータをまとめて取得することです。

ステップ 3: スロークエリログを有効にする

一度設定して動かしたままにしてください。時間のしきい値を超えたクエリをバックグラウンドで記録し、存在を知らなかった問題を浮かび上がらせます。実行時に有効にします。
ログファイルがどこに書かれているかを確認します。
ログを要約し、どのクエリが最も頻繁に現れるか、または合計時間が最も長いかを見るには、mysqldumpslow を使います。
long_query_time を 0.1 または 0.5 に下げると、個々には速いものの、多くの呼び出しで積み重なるクエリを捕まえられます。

ステップ 4: SHOW PROCESSLIST を確認する

何かが今まさに遅いとき、つまり実行時間が長すぎるクエリや、今ハングしているアプリケーションがあるときに使います。SHOW PROCESSLIST は、すべてのアクティブな接続と、各クエリがどれだけの時間実行されているかを示します。
Waiting for table metadata lock または Waiting for lock の状態で止まっているクエリは、不適切なプランのせいで遅いのではなく、ロックを保持する別のクエリにブロックされています。ロック競合の詳細は、次で確認します。
TRANSACTIONS セクションは、どのトランザクションが待っていて、どれがロックを保持しているかを示します。

クイックリファレンス

このページのすべての診断は、拡張機能を備えた MySQL のドロップイン代替である VillageSQL で動くため、チューニングのワークフローを学び直すことなく採用できます。

関連項目