同じSQLなのに遅い?MySQLの実行計画が変わる理由

MySQLについて、なぜ同じSQLでも実行計画が変わるのか

昨日まで高速だったSQLが、今日突然遅くなる。
アプリもコードも変えていないのに、急にページが開かなくなる。

MySQLを運用していると、この現象は珍しくありません。
そして多くの人が「サーバが重い」「インデックスが壊れた」と疑います。

しかし原因はそれではありません。

MySQLはSQLを実行しているのではなく、実行“方法”を毎回選び直しているからです。

この“実行方法”が「実行計画(execution plan)」です。
同じSQLでも実行計画が変わると、速度は桁単位で変わります。

実行計画とは何か

SQLは宣言型言語です。
「どうやって探すか」は書きません。

SELECT * FROM users WHERE email = 'a@example.com';

このSQLには、

  • インデックスを使え
  • 1件だけ探せ
  • テーブル全体を見ろ

といった指示はありません。
代わりにMySQLのオプティマイザが決めます。

具体的には次のような選択肢があります。

  • フルテーブルスキャン
  • インデックススキャン
  • インデックスレンジスキャン
  • 結合順序の決定
  • 一時テーブルの使用

この選択結果が実行計画です。

確認するには EXPLAIN を使います。

EXPLAIN SELECT * FROM users WHERE email = 'a@example.com';

ここで type が ALL ならフルスキャン、ref や range ならインデックス利用です。

なぜ同じSQLでも変わるのか

ここが重要です。

MySQLは「固定の処理」をしているわけではありません。
SQLが来るたびに、毎回オプティマイザが実行計画を計算しています。

つまり、

  • データ量
  • 分布
  • 統計情報
  • バッファ状態

が変われば、判断も変わります。

特に影響が大きいのはテーブルの行数と分布です。

例えばユーザーテーブルが1000件のとき、MySQLはインデックスを使います。
しかし1000万件になったとき、条件によってはフルスキャンの方が速いと判断することがあります。

これは間違いではありません。
MySQLの「推定」が変わっただけです。

JOINで突然遅くなる理由

実務で最も問題になるのがJOINです。

SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';

このSQLはテーブル結合順序が重要です。

MySQLは

  • users を先に絞るか
  • orders を先に読むか

を毎回決めています。

もし「activeユーザーが少ない」と推定すれば users → orders の順になります。
しかし統計情報がズレると、orders を全走査してから users を見る計画に変わります。

ここで性能が崩壊します。

SQLは同じでも、実際に読んでいる行数が100倍以上変わるからです。

キャッシュも影響する

さらに混乱させる要因があります。
キャッシュです。

MySQLは

  • バッファプール
  • インデックスページ
  • クエリキャッシュ(旧)

を使っています。

一度実行したSQLは、データがメモリに載ります。
すると2回目は速くなります。

つまり

  • 初回は遅い
  • 次は速い

が起きます。

これは実行計画ではなくI/Oの差ですが、現場では「不安定」に見えます。

実行計画が変わるタイミング

実行計画はランダムに変わるわけではありません。
典型的なタイミングがあります。

  • データの大量INSERT
  • 大量DELETE
  • メンテナンス後
  • サーバ再起動
  • ANALYZE TABLE 実行後
  • MySQLバージョンアップ

特にバージョンアップは要注意です。
オプティマイザのアルゴリズムが変わるため、同じSQLでも別のプランになります。

実務で起きる典型的な事故

よくあるのがこれです。

「開発環境では速いのに本番だけ遅い」

これはサーバ性能ではありません。
データ分布の違いです。

開発環境は件数が少なく均一です。
本番は偏っています。

MySQLのオプティマイザは「平均」を見て判断するため、本番でのみ誤判断します。

対処法:SQLを書き直す前に見るべきもの

遅いSQLに遭遇すると、多くの人がまずSQLを書き直します。
しかし先にやるべきは実行計画の確認です。

EXPLAIN ANALYZE SELECT ...;

ここで見るべきポイントは3つです。

  • 想定行数(rows)
  • 実際行数
  • 使用インデックス

想定行数と実際行数が大きくズレていれば、原因は統計情報です。
SQLではありません。

ヒント句(optimizer hint)の罠

MySQLにはヒント句があります。

SELECT /*+ INDEX(users idx_email) */ *
FROM users
WHERE email = 'a@example.com';

これでインデックスを固定できます。
一時的には速くなります。

しかしこれは応急処置です。
データ分布が変わると、今度は逆に遅くなる可能性があります。

ヒントは「オプティマイザのバグ回避」には有効ですが、恒久対策にはなりにくいです。

ではどうすればいいか

重要なのは、MySQLを「決定的なシステム」だと思わないことです。

MySQLはルールベースではなくコストベースで動きます。
つまり常に「推定」で動いています。

だから、

  • SQLは同じ
  • インデックスも同じ

でも結果は変わります。

遅いSQLを見つけたとき、まず疑うべきはSQLの書き方ではありません。
「MySQLが何を根拠にその実行計画を選んだのか」です。

実行計画を読む習慣がつくと、原因調査の時間は大きく減ります。
チューニングとは、SQLを美しく書くことではなく、MySQLの判断を理解することに近い作業です。