Query Go
スロークエリの特定と対処 — slow log / pg_stat_statements
チューニング

スロークエリの特定と対処 — slow log / pg_stat_statements

遅いクエリを特定する方法。MySQL スロークエリログ・PostgreSQL pg_stat_statements・auto_explain。

スロークエリの特定と対処 — slow log / pg_stat_statements diagram

まず「どのクエリが重いか」を知る

チューニングの鉄則は「遅いものから直す」です。「なんとなく遅い気がする」クエリをいじるより、実測で上位のクエリを順に潰すほうが投資対効果が圧倒的に高い。

探すべきは 2 種類:

  • 1 回あたりが遅いクエリ(レイテンシ観点)
  • 合計時間が大きいクエリ(スループット / DB 負荷観点)。1 回は速くても 1 秒 1000 回飛んでいるなら、それが DB を食っている

MySQL: スロークエリログ

MySQL にはスロークエリログがあり、一定時間以上かかったクエリを自動でファイルに記録できます。設定:

  • slow_query_log = ON
  • long_query_time = 1(秒。0.1 など少数も可)
  • slow_query_log_file = /var/log/mysql/slow.log
  • log_queries_not_using_indexes = ON(インデックス未使用も記録、ただし量が増えるので開発環境向け)

ログは mysqldumpslowpt-query-digest(Percona Toolkit)で集計するのが定番。同じパターンのクエリをまとめて、合計時間・呼び出し回数ランキングを出せます。

sql
-- 実行中のセッションで有効化
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;

-- 集計 (シェル)
-- $ pt-query-digest /var/log/mysql/slow.log | head

PostgreSQL: pg_stat_statements

PostgreSQL にはpg_stat_statementsという拡張があり、クエリをパラメータ正規化した単位で集計してくれます。呼び出し回数、合計・平均時間、取得行数、バッファヒット率まで一発で見える超便利ツール。本番で必ず有効化しましょう。

代表的な集計クエリ:

  • total_exec_time 降順 → DB 全体で最も重いクエリ
  • mean_exec_time 降順 → 1 回あたり遅いクエリ
  • calls 降順 → 高頻度クエリ(N+1 発見にも使える)
sql
-- 有効化 (postgresql.conf: shared_preload_libraries='pg_stat_statements')
CREATE EXTENSION pg_stat_statements;

-- 合計時間で上位 10 クエリ
SELECT
  round(total_exec_time::numeric, 1) AS total_ms,
  calls,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  substring(query, 1, 80) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

ボトルネック発見から修正までの流れ

実務での典型フローはこうなります:

  1. 上位リストを出す: pg_stat_statements / slow log で重いクエリ Top 10 を特定
  2. EXPLAIN ANALYZE で計画を見る(推定と実測の乖離、Seq Scan / filesort / temporary 等に注目)
  3. 仮説: インデックス不足 / 列順ミス / N+1 / 型キャスト / OR 条件分解不足 / 統計情報古い…
  4. 試しに修正: 開発環境で CREATE INDEX CONCURRENTLY やクエリ書き換えを適用し、再度 EXPLAIN ANALYZE で検証
  5. 本番適用: 負荷時間帯を避け、CONCURRENTLY / オンライン DDL で安全に投入
  6. 再計測: 翌日、pg_stat_statements の該当クエリが下がっているか確認

アプリ側の指標も見る

DB 単体のメトリクスだけ見ていると、アプリ側で組み立てた後に遅くなっているケースを見逃します。例えば:

  • 1 クエリは速いが、N+1 で 200 回発行されている
  • アプリが大量に取得した行をメモリ上でフィルタしている(DB に WHERE を任せれば一発)
  • ネットワーク往復 RTT がボトルネック

APM(Datadog / New Relic / Sentry Performance / OpenTelemetry)で「1 リクエスト中の DB 時間」をまず見るのが確実です。DB 側だけ最適化しても、ネットワークやアプリが原因ならレスポンスは改善しません。