画面の表示が遅い、バッチが終わらない、DB サーバーの CPU が張り付いている。こうしたとき、原因の多くは一部の重いクエリにあります。この記事では MySQL で遅いクエリ (スロークエリ) を見つける方法を、設定、ログの読み方、集計、RDS・Aurora での確認の順に説明します。
スロークエリとは
スロークエリは実行に時間のかかったクエリのことです。MySQL では、long_query_time に設定した秒数より長くかかったクエリをスロークエリとして数え、スロークエリログを有効にしていればファイルやテーブルに記録します。
何秒からが遅いかに決まった基準はありません。画面から呼ばれるクエリなら 0.5 秒でも遅すぎることがありますし、夜間の集計バッチなら 1 分かかっても問題ないこともあります。MySQL の既定値は 10 秒ですが、Web サービスで使うには長すぎることが多いので、多くの環境では下げて使います。
もう一つ大事なのは、1 回の実行時間と、合計の負荷は別だという点です。1 回 3 秒かかるクエリが 1 日に 10 回しか実行されないなら、合計は 30 秒です。一方、1 回 50 ミリ秒のクエリが 1 日に 10 万回実行されれば、合計は 5,000 秒を超えます。DB 全体の負荷を下げたいなら、後者を先に直すほうが効きます。スロークエリログは前者を見つけるのが得意で、後者は後で説明する performance_schema のほうが見つけやすい、という違いがあります。
また、long_query_time は CPU 時間ではなく実時間で測ります。DB が混み合っているときは、普段は速いクエリもロック待ちや I/O 待ちで遅くなり、ログに出てきます。ログに出たクエリが原因なのか、ほかの原因で遅くなった結果なのかは、分けて考える必要があります。
スロークエリのおもな原因
遅くなる理由はいくつかの型に分かれます。見つけたクエリがどれに当たるかを考えると、直し方の見当がつきます。
| 原因 | 典型的な症状 |
|---|---|
| 使えるインデックスがない | 返す行は少ないのに、読んだ行数 (Rows_examined) が桁違いに多い |
| インデックスがあっても使われない | 列に関数をかけている、型が合わない、LIKE '%語' の前方一致でない検索など |
| データ量の増加 | 以前は速かったクエリが、テーブルが大きくなるにつれて少しずつ遅くなる |
| ソートや集計の一時テーブル | ORDER BY や GROUP BY で大量の行を並べ替えている |
| ロック待ち | 実行時間は長いのに読んだ行数は少ない。ほかのトランザクションの更新を待っている |
| アプリの呼び方 | 1 回は速いが、ループの中で何千回も呼ばれている (N+1 問題) |
最後のアプリの呼び方はスロークエリログにはまず出てきません。1 回あたりは速いからです。これについては N+1 問題の記事 で詳しく扱っています。
遅いクエリを見つける 2 つの方法
MySQL で遅いクエリを探す方法は、大きく 2 つあります。
| スロークエリログ | performance_schema / sys スキーマ | |
|---|---|---|
| 記録するもの | しきい値を超えた個々の実行 | クエリの型ごとの累積の統計 |
| 既定の状態 | 無効 | MySQL 8.0 以降は有効 |
| 向いている調べ方 | 何時何分にどのクエリが遅かったか | どのクエリが DB 全体の時間を多く使っているか |
| 実際の値 | ログに残る | 代表例 (サンプル) のみ |
| 注意点 | ファイルが大きくなる。しきい値未満は残らない | サーバー起動からの累積。再起動で消える |
両方を使い分けるのが実用的です。障害の時間帯に何が起きていたかを調べるならスロークエリログ、普段の負荷を減らすための改善対象を選ぶなら performance_schema が向いています。
スロークエリログを有効にする
いまの設定を確認する
まず現在の設定を確認します。
SHOW GLOBAL VARIABLES WHERE Variable_name IN (
'slow_query_log', 'slow_query_log_file', 'long_query_time',
'log_output', 'min_examined_row_limit', 'log_queries_not_using_indexes'
);
slow_query_log が OFF なら記録されていません。log_output は記録先で、既定は FILE です。TABLE にすると mysql.slow_log テーブルに書かれ、NONE だとログを有効にしても何も書かれません。
ログを有効にしていなくても、しきい値を超えたクエリの件数は数えられています。
SHOW GLOBAL STATUS LIKE 'Slow_queries';
この値が時間とともに増えていれば、long_query_time を超えるクエリが実際に発生しています。
動的に有効にする
再起動せずに有効にできます。
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log = ON;
ここでつまずきやすいのが、long_query_time にはグローバルの値と接続 (セッション) ごとの値がある点です。SET GLOBAL で変えた値は変更後に作られた接続にだけ使われます。アプリがコネクションプールで接続を張りっぱなしにしていると、その接続は古いしきい値 (既定なら 10 秒) のままです。確認するときは新しい接続を作ってから見てください。
SELECT @@GLOBAL.long_query_time, @@SESSION.long_query_time;
SET GLOBAL の変更は再起動で元に戻ります。残したい場合は、MySQL 8.0 以降なら SET PERSIST を使うか、設定ファイルに書きます。
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_slow_extra = ON
log_slow_extra (MySQL 8.0.14 以降) を有効にすると、ファイル出力に読み取った行数の内訳、ソートの回数、ディスク上に作った一時テーブルの数などが加わります。テーブル出力には影響しません。
しきい値の決め方
最初は 1 秒程度から始め、ログの量を見ながら下げていくのが無難です。いきなり 0 (すべてのクエリを記録) にすると、ログが急に大きくなり、書き込みそのものが負荷になります。0 にするのは、特定の時間帯を短く調べるときだけにして、終わったら戻します。
log_queries_not_using_indexes を有効にすると、インデックスを使わないクエリが実行時間にかかわらず記録されます。小さなマスタテーブルの全件読み込みまで出てくるため、ログが埋まりやすい設定です。使うときは log_throttle_queries_not_using_indexes で 1 分あたりの記録数に上限をつけておくと安心です。
スロークエリログの読み方
ファイル出力のログは1 件ごとに次のような形で記録されます (値は例です)。
# Time: 2026-10-28T03:12:45.123456Z
# User@Host: app[app] @ [10.0.1.23] Id: 18342
# Query_time: 2.734120 Lock_time: 0.000012 Rows_sent: 20 Rows_examined: 1843921
use shop;
SET timestamp=1761621165;
SELECT id, total, created_at FROM orders WHERE customer_email = 'xxx@example.com' ORDER BY created_at DESC LIMIT 20;
見るべき値は 4 つです。
Query_time は実行にかかった秒数です。ロックを取るまでの最初の待ち時間は含まれず、Lock_time に別に記録されます。Rows_sent はクライアントに返した行数、Rows_examined はサーバーが読んだ行数です。
この例では 20 行を返すために 184 万行を読んでいます。Rows_examined が Rows_sent より桁違いに大きいクエリは、インデックスがない、または使われていない可能性が高く、改善の効果が出やすい候補です。この例なら customer_email にインデックスがあるかを確かめます。
逆に、Query_time は長いのに Rows_examined が小さい場合は、クエリ自体よりも待ちを疑います。同じ時間帯にほかのトランザクションが同じ行を更新していなかったか、ロックの状況もあわせて確認してください。ロック待ちの調べ方は ロック待ちの記事 で説明しています。
もう一つ知っておきたいのは、ログに書かれる順番です。MySQL はクエリが終わってロックを解放した後でログに書くため、ログの並びは実行の順番と一致しないことがあります。時刻を突き合わせるときは SET timestamp の値 (実行を始めた時刻) を見ます。
mysqldumpslow でログを集計する
ログが数千件あると、1 件ずつ読むのは現実的ではありません。MySQL に付属する mysqldumpslow を使うと、数値や文字列だけが違うクエリを N や 'S' に置き換えてまとめ、型ごとに集計できます。
# 合計時間の多い順に上位 10 件
mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log
# 実行回数の多い順に上位 10 件
mysqldumpslow -s c -t 10 /var/log/mysql/mysql-slow.log
# orders テーブルに関係するものだけ
mysqldumpslow -s t -t 10 -g 'orders' /var/log/mysql/mysql-slow.log
並び順を指定しないと、平均時間の順 (-s at) になります。1 回だけ極端に遅かったクエリが上位に来やすいので、負荷の大きいものから直したいなら -s t (合計時間) を使うのがおすすめです。
出力は次のような形です。
Count: 1240 Time=1.85s (2294s) Lock=0.00s (0s) Rows=20.0 (24800), app[app]@[10.0.1.23]
SELECT id, total, created_at FROM orders WHERE customer_email = 'S' ORDER BY created_at DESC LIMIT N
Count が回数、Time が平均時間で、かっこ内が合計です。この例では、平均 1.85 秒のクエリが 1,240 回実行され、合計 2,294 秒を使っています。
ログの抜き出しにはPercona Toolkit の pt-query-digest もよく使われます。こちらは読み取った行数の分布なども集計でき、ログが大きい環境で便利です。
performance_schema と sys スキーマで探す
スロークエリログを有効にしていなくても、MySQL 8.0 以降では performance_schema がクエリの型 (ダイジェスト) ごとの統計を集めています。しきい値に関係なく、すべてのクエリが対象です。
合計時間の多いクエリを出す
SELECT
SCHEMA_NAME,
LEFT(DIGEST_TEXT, 100) AS query,
COUNT_STAR AS calls,
FORMAT_PICO_TIME(SUM_TIMER_WAIT) AS total_time,
FORMAT_PICO_TIME(AVG_TIMER_WAIT) AS avg_time,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT,
SUM_NO_INDEX_USED
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
時間の列の単位はピコ秒なので、FORMAT_PICO_TIME() (MySQL 8.0.16 以降) で読みやすい形に変えています。SUM_NO_INDEX_USED はインデックスを使わずに実行された回数です。冒頭で書いた「1 回は速いが回数が多い」クエリは、この一覧で上位に出てきます。
実際の値が入った例文は QUERY_SAMPLE_TEXT 列で見られます。そのまま EXPLAIN に使えるので便利ですが、顧客のメールアドレスなどがそのまま入っていることがある点には注意してください。
sys スキーマのビューを使う
sys スキーマは、performance_schema の情報を人が読みやすい形にまとめたビューの集まりです。同じことをもっと短く書けます。
-- 合計時間の多い順 (既定の並び順)
SELECT query, db, exec_count, total_latency, avg_latency,
rows_examined_avg, rows_sent_avg, full_scan
FROM sys.statement_analysis
LIMIT 10;
-- フルテーブルスキャンをしているクエリ
SELECT query, db, exec_count, total_latency,
no_index_used_pct, rows_examined_avg, rows_sent_avg
FROM sys.statements_with_full_table_scans
LIMIT 10;
ほかに、実行時間が上位 5% に入るクエリを出す sys.statements_with_runtimes_in_95th_percentile もあります。
統計を読むときの注意
performance_schema の数字は、サーバーが起動してからの累積です。昨日の夜に何が遅かったかを知るには向きません。改善の効果を確かめたいときは、改善前に次の文で統計をリセットし、しばらく経ってから見直します。
TRUNCATE TABLE performance_schema.events_statements_summary_by_digest;
また、ダイジェストの表には上限があり、いっぱいになると、それ以降の新しい型のクエリは DIGEST が NULL の 1 行にまとめられます。この行の COUNT_STAR が全体の大きな割合を占めているなら、performance_schema_digests_size を増やす必要があります (起動時にしか変えられません)。
RDS・Aurora での確認
Amazon RDS for MySQL と Aurora MySQL では、設定ファイルの代わりに DB パラメータグループで slow_query_log、long_query_time、log_output を設定します。SET GLOBAL は使えません。
気をつけたいのは、log_output の既定値が RDS と Aurora で違うことです。
| RDS for MySQL | Aurora MySQL (バージョン 2・3) | |
|---|---|---|
log_output の既定 |
TABLE |
FILE |
| ファイルの保持 | 2 週間 (容量の上限あり) | 24 時間 |
| CloudWatch Logs への出力 | FILE のときだけ可能 |
FILE のときだけ可能 |
TABLE のとき、long_query_time は秒単位の整数しか指定できません。0.5 秒のような小数を使いたいなら FILE にします。
TABLE の場合は SQL で中身を見られます。sql_text はバイナリ型なので、文字コードを変換して読みます。
SELECT start_time, query_time, lock_time, rows_sent, rows_examined, db,
CONVERT(sql_text USING utf8mb4) AS sql_text
FROM mysql.slow_log
WHERE start_time >= NOW() - INTERVAL 1 HOUR
ORDER BY query_time DESC
LIMIT 20;
テーブルが大きくなったら CALL mysql.rds_rotate_slow_log; で入れ替えます。入れ替え前の中身は mysql.slow_log_backup に残ります。
Aurora はファイルを 24 時間で消すため、後から調べたいなら CloudWatch Logs に出力しておくのが現実的です。CloudWatch Logs Insights を使えば、時間帯や文字列で絞り込めます。
画面でクエリごとの負荷を見たい場合は、CloudWatch Database Insights を使います。以前の Performance Insights は 2026 年に提供を終え、Database Insights に置き換わりました。DB の負荷を SQL ごとに分けて見る機能は Standard モードでも使えますが、スロークエリの分析は Advanced モードの機能で、ログを CloudWatch Logs に出力しておく必要があります (2026 年 11 月時点、AWS 公式ドキュメントより)。
performance_schema のクエリを RDS で使う場合は、まず有効かどうかを確かめてください。Performance Insights (現在の Database Insights) を有効にしたインスタンスでは自動で有効になりますが、そうでない場合は無効のことがあります。performance_schema の変更には再起動が必要です。
SHOW GLOBAL VARIABLES LIKE 'performance_schema';
スロークエリログに出てこないときの確認
設定したはずなのに記録されない場合は、次の順に確かめると原因が見つかりやすくなります。
log_outputがNONEになっていないか。FILEなのにテーブルを見ている (またはその逆) こともよくあります。- 見ている接続のしきい値が古くないか。
SET GLOBALの前から張られている接続には、新しいしきい値が使われません。 min_examined_row_limitが 0 より大きくないか。読んだ行数がこの値より少ないクエリは、遅くても記録されません。- 管理用の文ではないか。
ALTER TABLEやCREATE INDEXなどはlog_slow_admin_statementsを有効にしないと記録されません。 - レプリカで見ていないか。レプリカは複製されたクエリを既定では記録しません。
スロークエリを見つけたあとにやること
見つけたクエリは、次の順で手をつけると無駄が少なくなります。
まず、合計時間の多いものから選びます。平均が遅いものより、DB 全体の時間を多く使っているもののほうが、直したときの効果が大きいからです。
次に、そのクエリを EXPLAIN にかけて、どのインデックスを使い、何行読むと見積もられているかを確かめます。type が ALL (フルスキャン) になっていたり、rows が返す行数よりずっと大きかったりすれば、インデックスの追加や条件の書き方を見直します。読み方は MySQL の EXPLAIN の記事 で詳しく説明しています。
インデックスを追加したら、同じクエリで Rows_examined や実行時間が下がったかを確かめます。performance_schema の統計をリセットしておくと、変更の前後を比べやすくなります。
この確認を続けるには
スロークエリは一度直して終わりではありません。データが増えたり、新しい機能が入ったりすると、別のクエリが遅くなります。手作業で続けるなら、週に一度 mysqldumpslow -s t や sys.statement_analysis の上位を見る習慣をつけるのが手軽です。cron で集計結果をチームのチャットに送ったり、監視ツールで Slow_queries の増え方にアラートをかけたりする方法もあります。
AI に任せる方法もあります。Scandock は接続文字列を登録すると AI がテーブルの構成と実データを読み、何を監視するかを決めるサービスです。固定の閾値ではなく値の推移や過去の経緯を踏まえて異常かどうかを判断し、異常と判断すると読み取り専用のクエリで関連するデータを調べ、原因・影響・根拠・対処案をつけて通知します。対応 DB には MySQL、MariaDB、Amazon RDS が含まれます。
参考
- MySQL 8.4 Reference Manual: The Slow Query Log
- MySQL 8.4 Reference Manual: Server System Variables
- MySQL 8.4 Reference Manual: mysqldumpslow
- MySQL 8.4 Reference Manual: Statement Summary Tables
- MySQL 8.4 Reference Manual: The statement_analysis and x$statement_analysis Views
- MySQL 8.4 Reference Manual: The statements_with_full_table_scans and x$statements_with_full_table_scans Views
- Amazon RDS ユーザーガイド: Overview of RDS for MySQL database logs
- Amazon Aurora ユーザーガイド: Overview of Aurora MySQL database logs
- Amazon CloudWatch ユーザーガイド: CloudWatch Database Insights
- Amazon RDS ユーザーガイド: Turning on the Performance Schema for Performance Insights on Amazon RDS for MariaDB or MySQL