「MySQL が遅い」と言われたとき、最初に見るべきなのはサーバ設定ではなく、実際に時間を使っている SQL です。遅いクエリを特定し、実行計画を確認し、必要に応じてインデックスやロック状況を見ていくと、原因を切り分けやすくなります。
この記事では、MySQL のクエリ性能問題を調べるときに、最初に確認したいポイントを整理します。PostgreSQL 版は「PostgreSQL のクエリが遅い原因を突き止める手順」をご覧ください。
参考にした公式ドキュメントは MySQL 8.4 です。基本的な考え方は MySQL 8.0 系でも共通する部分が多いですが、利用できる機能や表示内容はバージョンによって異なる場合があります。
ステップ1: スロークエリログで遅いクエリを特定する
スロークエリログの設定
スロークエリログは、時間のかかっている SQL を見つけるための基本的な手段です。初期状態では無効になっていることがあるため、必要に応じて my.cnf(または my.ini)で設定します。
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # 1秒以上かかったクエリを記録
log_queries_not_using_indexes = ON # インデックス未使用クエリも記録(調査時のみ推奨)
log_queries_not_using_indexes は便利ですが、ログ量が急増することがあります。本番環境で常時有効にするのではなく、調査目的で短時間だけ使うか、必要に応じて log_throttle_queries_not_using_indexes で記録数を制限します。
設定ファイルを変更したくない場合は、実行中の MySQL に一時的に設定できます。
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
mysqldumpslow でまとめて確認する
ログが蓄積されたら mysqldumpslow でサマリを見ます。
# 実行時間の長い順に上位10件
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
performance_schema を使う
performance_schema からも、正規化された SQL ごとの実行回数や合計時間を確認できます。環境によっては statement event の収集が一部無効になっているため、値が取れない場合は Performance Schema の設定も確認します。
SELECT
digest_text,
count_star AS calls,
round(avg_timer_wait / 1e9, 2) AS avg_ms,
round(sum_timer_wait / 1e9, 2) AS total_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC
LIMIT 10;
ステップ2: EXPLAIN で実行計画を読む
調査対象のクエリが特定できたら EXPLAIN で実行計画を確認します。
EXPLAIN SELECT * FROM orders WHERE customer_id = 12345;
MySQL 8.0.18 以降では EXPLAIN ANALYZE も使えます。
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 12345;
注目する列
| 列 | 確認ポイント |
|---|---|
type | ALL(フルスキャン)なら、対象テーブルの大きさと取得件数に対して妥当か確認 |
key | 使用されているインデックス名。NULL なら未使用 |
rows | スキャンする推定行数。大きいほど遅い |
Extra | Using filesort・Using temporary が遅さに関係していないか確認 |
type: ALL はテーブル全体を読む実行計画です。ただし、小さなテーブルや大部分の行を返すクエリでは妥当な場合もあります。大きなテーブルで少数行だけを取得したいのに ALL になっている場合は、優先して見直します。
よくある原因と対処
原因1: インデックスが使われていない
-- インデックスの存在を確認
SHOW INDEX FROM orders;
-- インデックスを追加
ALTER TABLE orders ADD INDEX idx_customer_id (customer_id);
複合インデックスのカラム順序は重要です。WHERE customer_id = ? AND status = ? に対しては (customer_id, status) の順で定義するのが候補になります。ただし、最適な順序は絞り込みの強さ、並び替え、結合条件によって変わります。また、インデックスの先頭カラムを条件に含まない場合、期待どおりに使われないことがあります。
原因2: ロックの競合
SHOW PROCESSLIST で実行中のクエリとロック待ちを確認します。
SHOW FULL PROCESSLIST;
State 列に Waiting for table metadata lock などが出ていたら、DDL や長時間トランザクションの影響で待たされている可能性があります。Lock wait timeout exceeded は待ちが長引いた結果として返るエラーであり、SHOW PROCESSLIST の State にそのまま出るとは限りません。
-- InnoDB のトランザクション状況を確認
SELECT * FROM information_schema.INNODB_TRX;
長時間実行中のトランザクションが原因と判断できる場合は、影響範囲を確認した上で KILL [Id] による強制終了を検討します。
原因3: innodb_buffer_pool_size が小さい
InnoDB はデータをメモリ上のバッファプールにキャッシュして読み書きします。バッファプールが小さいとディスク I/O が増え、全体的に遅くなります。
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
Innodb_buffer_pool_read_requests は論理読み取り回数、Innodb_buffer_pool_reads はバッファプールで満たせずディスクから読んだ回数です。次のようにヒット率を計算し、ディスク読み取りが多いかを確認します。
SELECT
1 - (
MAX(CASE WHEN variable_name = 'Innodb_buffer_pool_reads' THEN variable_value + 0 END) /
NULLIF(MAX(CASE WHEN variable_name = 'Innodb_buffer_pool_read_requests' THEN variable_value + 0 END), 0)
) AS buffer_pool_hit_ratio
FROM performance_schema.global_status
WHERE variable_name IN ('Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests');
専用サーバでは物理メモリの 80% 程度まで割り当てる例もありますが、同じサーバでアプリケーションや他のプロセスも動いている場合は別です。OS のページングを起こさない範囲で調整します。
innodb_buffer_pool_size = 4G # サーバの空きメモリに合わせて設定
原因4: ORDER BY / GROUP BY の filesort
EXPLAIN の Extra に Using filesort が出ていたら、インデックス順の読み取りだけでは並び替えを処理できず、追加のソート処理が発生しています。ただし、件数が少ない場合は問題にならないこともあります。遅いクエリで出ている場合は、WHERE と ORDER BY の組み合わせに合う複合インデックスを検討します。
-- WHERE customer_id = ? ORDER BY status の filesort を抑える例
ALTER TABLE orders ADD INDEX idx_customer_status (customer_id, status);
チェックリスト
- スロークエリログで遅いクエリを特定した
-
EXPLAINでtype: ALLが妥当か確認した -
SHOW PROCESSLISTでロック待ちのクエリを確認した - バッファプールのヒット率を確認した
- 必要なインデックスを追加した
おわりに
MySQL の性能問題は、「どのクエリが遅いか」→「なぜ遅いか」の順で調べることが基本です。スロークエリログや performance_schema で対象を絞り、EXPLAIN で実行計画を確認し、必要に応じてインデックス、ロック、バッファプールを見ていくと、原因を整理しやすくなります。
原因特定から対処まで対応しています。障害調査・原因分析サービスもご覧ください。