MySQL パフォーマンスチューニング実践 2026:EXPLAIN、索引最適化、クエリ高速化

数据库

なぜ 2026 年も MySQL チューニングにこだわるか

NewSQL やベクトル DB が台頭しても、MySQL は世界の OLTP トラフィックの大半を支えています。多くの「遅い」問題はハードウェア不足ではなく、索引欠落・実行計画の逸脱・接続管理の混乱が原因です。本稿は実行可能なチューニングに焦点を当てます。

症状 常な根因
たまに停滞 索引欠落、全表スキャン
使うほど遅い バッファプール過小、ディスク IO 急増
ピークで崩壊 接続数暴走、スレッド競合
単発 SQL タイムアウト 深いページネーション / 不適切 JOIN

ステップ1:EXPLAIN で実行計画を見る

チューニングは必ず EXPLAIN から。typekeyrowsExtra に注目。

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 ツールが役立ちます:


MySQL チューニングに銀の弾丸はないが、方法論はある。EXPLAIN でボトルネックを見つけ、索引とバッファプールで IO を解消し、接続プールでピークを支える。この三層を正せば、大半のスロークエリは消えます。

ブラウザローカルツールを無料で試す →

#MySQL#性能调优#索引#EXPLAIN#InnoDB#数据库