データベース

MySQLスロークエリログの使い方|遅いSQLを記録し、合計時間と回数で絞る

MySQL 8.4でスロークエリログを一時的に有効化する手順。long_query_timeのGLOBALとSESSIONの違い、ログ各項目の読み方、mysqldumpslowの集計、元の設定へ戻す方法を実測例で解説します。

この記事の目次
  1. 現在値を保存し、短時間だけFILE出力を有効にする
  2. ログが出ないときは、接続ごとの閾値と走査行数を見る
  3. 生ログの1件から、時間・返却行数・走査行数を読む
  4. mysqldumpslowで、合計時間・回数・平均時間を見比べる
  5. 確認が終わったら、同じ接続で元の設定へ戻す
  6. 次に調べるSQLを、収集条件と一緒に記録する

MySQLで遅いSQLを探すなら、スロークエリログに一定時間を超えたSQLを残し、同じ形のSQLごとに集計します。1回が遅いSQLと、何度も動いて合計時間が大きくなるSQLの両方を見ると、次に調べる対象を決めやすくなります。

最初に押さえたいのは、slow_query_log = ONだけでは記録条件が揃わないことです。出力先、時間の閾値、調べた行数の条件を確認します。また、GLOBALの閾値を変えても、すでに接続しているアプリのSESSION値は変わりません。

以下はMySQL 8.4.11の独立した検証環境で実行した例です。短い待機SQLで記録を確認し、集計後に設定を元へ戻します。実際のDBで使うときは、ログを採る時間帯・容量・閲覧権限を決めてから進めてください。SQL本文に検索値などが含まれるため、生ログをそのまま外部へ共有しないようにします。

現在値を保存し、短時間だけFILE出力を有効にする

管理用のSQL接続で、次の順に実行します。この例はslow_query_logとgeneral_logがどちらもOFFの検証環境を前提としています。すでに収集している環境では、既存のログ運用と調査担当を確認し、その設定を一律に置き換えないでください。

log_outputはgeneral logとslow logで共有する設定です。general logが稼働中にFILEへ変更すると、そちらの出力先にも影響します。出力先の関係はMySQL公式のログ出力先の説明で確認できます。

まず現在値を表示し、結果を手元の記録にも残します。続く@old_...は、同じ接続で元へ戻すための保存値です。接続を閉じると消えるので、復元が終わるまでこの接続を保ちます。

-- Run in one admin connection. Keep the displayed values outside the session too.
SELECT @@hostname, VERSION(), CONNECTION_ID();
SELECT @@GLOBAL.slow_query_log, @@GLOBAL.log_output,
       @@GLOBAL.slow_query_log_file, @@GLOBAL.long_query_time,
       @@SESSION.long_query_time, @@GLOBAL.min_examined_row_limit,
       @@SESSION.min_examined_row_limit,
       @@GLOBAL.log_queries_not_using_indexes, @@GLOBAL.general_log;

SET @old_slow = @@GLOBAL.slow_query_log;
SET @old_output = @@GLOBAL.log_output;
SET @old_global_time = @@GLOBAL.long_query_time;
SET @old_session_time = @@SESSION.long_query_time;
SET @old_global_rows = @@GLOBAL.min_examined_row_limit;
SET @old_session_rows = @@SESSION.min_examined_row_limit;
SET @old_no_index = @@GLOBAL.log_queries_not_using_indexes;

-- Lab precondition: slow_query_log=OFF and general_log=OFF.
-- Retain the existing slow_query_log_file path.
SET GLOBAL log_output = 'FILE';
SET GLOBAL long_query_time = 0.05;
SET GLOBAL min_examined_row_limit = 0;
SET GLOBAL log_queries_not_using_indexes = OFF;
SET GLOBAL slow_query_log = ON;

-- GLOBAL changes do not update this existing connection.
SELECT @@GLOBAL.long_query_time, @@SESSION.long_query_time;
SET SESSION long_query_time = 0.05;
SET SESSION min_examined_row_limit = 0;
SELECT SLEEP(0.12) AS frequent;
SELECT SLEEP(0.12) AS frequent;
SELECT SLEEP(0.12) AS frequent;
SELECT SLEEP(0.25) AS occasional;

0.05秒は、この短い再現用の値です。本番でそのまま採用するとログ量が増えるため、アプリの応答目標と負荷に合わせて決めます。long_query_timeの既定値は10秒です。GLOBAL値の変更には、SYSTEM_VARIABLES_ADMINなどの管理権限が必要です。

この例では既存のslow_query_log_fileを使い、ファイル名を変更していません。表示されたパスはMySQLが動くサーバー側のパスです。SQLクライアントを起動した手元のPCとは限りません。権限がなく設定を変更できない環境では、管理者やマネージドDBのログ機能で取得します。

スポンサーリンク

ログが出ないときは、接続ごとの閾値と走査行数を見る

検証ではGLOBALを0.05秒へ変更した直後も、同じ接続のSESSIONは10秒のままでした。その接続へ明示的にSET SESSION long_query_time = 0.05;を実行すると、0.12秒の待機SQLが記録されました。変更後に作った新しい接続も、GLOBALの0.05秒を引き継いで記録できています。

接続プールを使うアプリでは、前から開いている接続が残ります。SQLエディタで自分のSESSIONだけを変えても、アプリ接続の閾値は変わりません。アプリ側で使っている接続の値を確かめ、必要に応じて接続を作り直すなど、運用に合う反映方法を選びます。GLOBALとSESSIONの反映範囲も参照してください。

確認する項目 ログが出ない理由の例 次の確認
slow_query_log OFFになっている 対象サーバーと有効状態を確認
log_output TABLEのみ、またはNONE FILEを読んでよい設定か確認
@@SESSION.long_query_time 既存接続に以前の閾値が残る SQLを実行する接続自身で確認
@@SESSION.min_examined_row_limit 調べた行数が条件に届かない 時間だけで収集する検証では0を使う
slow_query_log_file 違うサーバーやパスを見ている 実値とMySQLの書き込み権限を確認
SQLの完了 まだ実行中でログへ出ていない 完了後の記録と、実行中の待機調査を分ける

min_examined_row_limitは、SQLが調べた行数の下限です。今回のSELECT SLEEP(...)は生ログ上でRows_examined: 1となり、この下限を2にすると記録されず、0に戻すと記録されました。テーブルを直接読まない文だから常に0行、と推測せず、実際のログで確認します。

log_queries_not_using_indexesをONにすると、行の検索にインデックスを使わないSQLも収集対象になります。検証では、時間の閾値を100秒にしても、3行を調べる短い検索が記録されました。まず遅いSQLを探したいならOFFから始め、インデックス未使用の調査が必要になったときに対象を広げると、目的とログ量を揃えやすくなります。

生ログの1件から、時間・返却行数・走査行数を読む

ログファイルを読める環境で、slow_query_log_fileの実値を指定します。次のパスは差し替え用です。Docker内にあるファイルなら、そのコンテナ内で確認するか、調査用に取得したコピーを読みます。

tail -n 40 /path/to/mysql-slow.log

以下は検証ログから、時間とSQLの行を抜き出したものです。待機時間を作るためのSLEEPであり、インデックス改善の効果を測った結果ではありません。

# Query_time: 0.120272  Lock_time: 0.000000 Rows_sent: 1  Rows_examined: 1
SET timestamp=1789183197;
SELECT SLEEP(0.12) AS frequent;
項目 読み取る内容
Query_time SQLの実行時間。単位は秒
Lock_time ロック取得に要した時間。これだけで全種類の待機を説明できるとは考えない
Rows_sent クライアントへ返した行数
Rows_examined サーバー層で調べた行数。ストレージエンジン内部の処理をすべて数えた値ではない

仕様と出力項目はMySQL公式のslow query logに記載されています。返した行数に対して調べた行数が多ければ、条件や実行計画を確認する手掛かりになります。ただし、集計処理などでは多くの行を読むこと自体が必要な場合もあり、この比率だけでインデックスを追加する判断はできません。

ログは実行を終え、ロックを解放した後に書かれます。今まさに止まっているSQLやロックを持つ接続を探したい場合は、ロック待ち・デッドロックの確認方法へ進みます。

mysqldumpslowで、合計時間・回数・平均時間を見比べる

収集範囲を決めたログのコピーを、mysqldumpslowで集計します。このコマンドは、数値や文字列が違うだけのSQLをまとめて表示します。mysqldumpとは別のツールです。見つからない場合は、使用中のMySQL配布パッケージに含まれるかを確認してください。

# ログに残ったSQLの合計時間が大きい順
mysqldumpslow -l -s t /path/to/mysql-slow.log

# ログに残った回数が多い順
mysqldumpslow -l -s c /path/to/mysql-slow.log

# 1回あたりの平均時間が大きい順
mysqldumpslow -l -s at /path/to/mysql-slow.log

-s tは合計時間、-s cは記録回数、-s atは平均時間の並べ替えです。ここでは-lも指定し、Query_timeからLock_timeを差し引かずに集計しています。オプションの定義はmysqldumpslowの公式リファレンスで確認できます。

先ほどの4回のSQLだけを含むログを集計すると、次のようになりました。Time=の直後は平均秒数、括弧内は合計秒数です。括弧内は整数表示のため、1秒未満の合計は0sに見えます。

Count: 3  Time=0.12s (0s)  Lock=0.00s (0s)  Rows=1.0 (3), root[root]@localhost
  SELECT SLEEP(N.N) AS frequent

Count: 1  Time=0.25s (0s)  Lock=0.00s (0s)  Rows=1.0 (1), root[root]@localhost
  SELECT SLEEP(N.N) AS occasional

合計時間順ではfrequentが先、平均時間順ではoccasionalが先になります。3回動く0.12秒のSQLは合計約0.36秒なので、1回だけ動く約0.25秒のSQLより上に来るわけです。

Countはログに残った回数であり、アプリ全体で実行された総回数ではありません。閾値より速かった実行は含まれないため、頻度を比較するときは収集期間と設定も揃えます。また、集計のRowsは返却行数に基づく値です。走査行数のRows_examinedを確認したいときは、対応する生ログへ戻ります。

今回使った最小構成のDockerイメージには集計ツールが入っていなかったため、検証では同じMySQL 8.4.11の公式ソースにあるmysqldumpslowをPerlで実行しました。サーバーで採ったログを、ツールのある調査用PCで集計する形でも構いません。

確認が終わったら、同じ接続で元の設定へ戻す

収集終了時は、冒頭で保存した値を表示します。どれかがNULLなら、そのまま復元SQLを流さず止めてください。別の接続を開いてしまった可能性があります。その場合は手元に記録した元の値を使い、管理対象の設定と照合して戻します。

-- Same connection as capture.sql; inspect saved values before changing anything.
SELECT @old_slow, @old_output, @old_global_time, @old_session_time,
       @old_global_rows, @old_session_rows, @old_no_index;
-- If a saved value is NULL, stop and use the separately recorded original values.
SET GLOBAL slow_query_log = OFF;
SET GLOBAL long_query_time = @old_global_time;
SET GLOBAL min_examined_row_limit = @old_global_rows;
SET GLOBAL log_queries_not_using_indexes = @old_no_index;
SET GLOBAL log_output = @old_output;
SET SESSION long_query_time = @old_session_time;
SET SESSION min_examined_row_limit = @old_session_rows;
SET GLOBAL slow_query_log = @old_slow;
SELECT @@GLOBAL.slow_query_log, @@GLOBAL.log_output,
       @@GLOBAL.long_query_time, @@SESSION.long_query_time,
       @@GLOBAL.min_examined_row_limit, @@SESSION.min_examined_row_limit,
       @@GLOBAL.log_queries_not_using_indexes;

この例の開始状態はslow logがOFFなので、最後もOFFへ戻ります。ログの停止と、過去のログの削除は別です。残ったファイルは閲覧権限・保管期間・ローテーションの方針に従って扱います。

今回の検証では、GLOBALと管理接続のSESSION値が開始前と一致し、復元後に追加した待機SQLでログが増えないことまで確認しました。調査中に作られたほかの接続には、その接続自身のSESSION値が残る点にも注意してください。継続運用へ戻す際は、アプリ側の接続管理も確認します。

次に調べるSQLを、収集条件と一緒に記録する

集計を見てすぐ設定を増やすより、調査対象を1つ選び、生ログと実行計画を照合できる状態にします。次の表を埋めると、後日の比較で閾値や収集時間が違っていた、という取り違えを防げます。

記録項目 残すこと
収集条件 サーバー、開始・終了時刻、時間と走査行数の閾値、インデックス未使用SQLの扱い
SQLの形 対象テーブルと条件。共有用の値は機密情報を除く
優先度 記録回数、平均時間、合計時間と、利用者への影響
生ログの手掛かり 代表例のQuery_time、Rows_sent、Rows_examined
次の調査 実行計画、取得列、呼び出し回数など、確かめる対象を1つ選ぶ
比較 変更後も同じ条件で採った結果と、アプリの応答時間

継続して収集するなら、一時調査を終えてから、保存容量とローテーションを含めた設定を決めます。起動時の設定ファイルの[mysqld]で管理する方法と、SET PERSISTで永続化する方法があります。両方へ無計画に書かず、現在の管理方法に揃えてください。変数の確認・変更の基本はMySQLのシステム変数で確認できます。

SQLの送信履歴そのものを見たい場合は、general logとDBeaverのQuery Managerが使えます。性能調査では、今回選んだSQLの読み取り量や呼び出し方を確認し、同じ条件で再測定するところへ進めましょう。

スポンサーリンク