MySQL パフォーマンスチューニング実践 2026:EXPLAIN、索引最適化、クエリ高速化
なぜ 2026 年も MySQL チューニングにこだわるか
NewSQL やベクトル DB が台頭しても、MySQL は世界の OLTP トラフィックの大半を支えています。多くの「遅い」問題はハードウェア不足ではなく、索引欠落・実行計画の逸脱・接続管理の混乱が原因です。本稿は実行可能なチューニングに焦点を当てます。
| 症状 | 常な根因 |
|---|---|
| たまに停滞 | 索引欠落、全表スキャン |
| 使うほど遅い | バッファプール過小、ディスク IO 急増 |
| ピークで崩壊 | 接続数暴走、スレッド競合 |
| 単発 SQL タイムアウト | 深いページネーション / 不適切 JOIN |
ステップ1:EXPLAIN で実行計画を見る
チューニングは必ず EXPLAIN から。type、key、rows、Extra に注目。
EXPLAIN SELECT id, user_id, amount
FROM orders
WHERE user_id = 10086 AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;
主要カラムの読み方:
| カラム | 健全値 | 危険信号 |
|---|---|---|
| type | ref / range / const | ALL(全表スキャン) |
| key | 想定索引 | NULL |
| rows | 実ヒット行に近い | 予想よりずっと大 |
| Extra | Using index | Using filesort / Using temporary |
複雑な SQL は先に SQL フォーマッター で整形し、ネストと JOIN 順を把握しやすくします。
索引設計の三原則
原則1:複合索引は最左前缀を守る
索引 (user_id, status, created_at) は user_id= にも user_id+status にも効くが、status 単体には効かない。
-- 索引利用
SELECT * FROM orders WHERE user_id = 1 AND status = 'PAID';
-- この複合索引の status 部は使えない(索引スキップスキャン除く)
SELECT * FROM orders WHERE status = 'PAID';
原則2:カバリング索引を優先
クエリ列が全て索引に含まれれば、MySQL は表参照(回表)を省略し、大幅に速くなります。
-- カバリング索引を作成
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, created_at);
-- Using index:索引から直接返却、回表なし
EXPLAIN SELECT user_id, status, created_at
FROM orders WHERE user_id = 10086;
原則3:選択度の高い列を前へ
user_id(選択度高)を複合索引の前へ、低選択度(値が少数の status 等)を後ろへ。
スロークエリログ:問題を自ら顕在化
-- スロークエリログを有効化(200ms 超を記録)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.2;
SET GLOBAL log_queries_not_using_indexes = 'ON';
pt-query-digest や内蔵 sys.statement_analysis ビューと組み合わせ、Top SQL を特定します。
InnoDB バッファプール調整
バッファプールは InnoDB の性能心臓。ホットデータをメモリに常駐させます。
-- バッファプール命中率を確認(> 99% が理想)
SHOW ENGINE INNODB STATUS\G
-- Buffer pool hit rate が 1000/1000 に近いほど良い
-- 本番:バッファプールを利用可能 RAM の 60%~75% に
-- my.cnf で:
-- innodb_buffer_pool_size = 12G
-- innodb_buffer_pool_instances = 8 -- マルチコア競合を低減
クエリ層の最適化
深いページネーション「LIMIT 100000, 20」を避ける
深いページネーションは 10 万行をスキャンして捨てます。**[カーソル / 遅延結合]**で書き換え:
-- アンチパターン
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
-- 最適化:主キーを先に取得し、再度結合
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 20) t ON o.id = t.id;
大きなリストには IN ではなく EXISTS
-- サブクエリ結果が大きい場合
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000);
必要な列だけを選ぶ
SELECT * は IO の無駄で、カバリング索引も無効にします。列名を明示するのが最も投資対効果の高い最適化です。
JOIN 最適化の要点
- 駆動表は小表を選び、被駆動表の結合列には索引を。
- 結合列の型は一致させる(暗黙変換は索引を無効化)。
- 大表 JOIN では次元列の非正規化を検討し、クロステーブル参照を減らす。
接続プール:接続がボトルネックにしない
アプリ側の接続管理の混乱がピーク崩壊の主因。Node.js 例:
import mysql from "mysql2/promise";
const pool = mysql.createPool({
host: "db.example.com",
user: "app",
database: "shop",
connectionLimit: 20, // 同時接続を制限
waitForConnections: true,
queueLimit: 50, // 上限超えの待機リクエスト数
enableKeepAlive: true
});
// 接続は常に try/finally で解放しリークを防ぐ
const [rows] = await pool.query("SELECT * FROM orders WHERE user_id = ?", [uid]);
Hash 計算 ツールでキャッシュキーを生成し、ホットクエリ結果をキャッシュして DB 負荷を下げます。
よくある質問 FAQ
Q1:索引を追加したのにまだ遅い?
おそらく:索引選択性が低い、統計情報が古い(ANALYZE TABLE)、クエリが最左前缀を辿っていない。EXPLAIN で実際の key を確認を。
Q2:索引は多いほど良い?
違います。索引は書き込みを遅くし容量を食います。「読みが多く書きが少ないホットクエリ」のみに作る。
Q3:バッファプールは大きいほど?
無盲に大きくはNG。物理 RAM 超えは swap を誘発し逆効果。通常は利用可能 RAM の 60%~75%。
Q4:VARCHAR 索引の注意点は?
長文字列はプレフィックス索引 INDEX(col(20)) を利用。ただしプレフィックス索引はカバリング走査の完全一致に使えず、权衡が必要。
Q5:シャード分割の目安は?
単表が数億行超、ホット行ロック競合が深刻、バックアップ窓が長すぎる場合のみ。まず索引とクエリ最適化を。
推奨ツール
MySQL チューニングには、以下の ToolsKu ツールが役立ちます:
- SQL フォーマッター — 複雑なクエリの整形・圧縮で EXPLAIN 分析
- JSON フォーマッター —
EXPLAIN FORMAT=JSON実行計画の整形 - ハッシュ計算 — キャッシュ層のクエリキャッシュキー生成
MySQL チューニングに銀の弾丸はないが、方法論はある。EXPLAIN でボトルネックを見つけ、索引とバッファプールで IO を解消し、接続プールでピークを支える。この三層を正せば、大半のスロークエリは消えます。
ブラウザローカルツールを無料で試す →