2026年5月23日

2026年5月23日

PostgreSQLのpg_stat_statementsでクエリ統計を取る|重いSQLの見つけ方と改善の確かめ方

PostgreSQL で「どの SQL がサーバーの時間を一番使っているか」を知るには、拡張機能 pg_stat_statements を使うのが近道です。実行された SQL を形ごとにまとめ、呼び出し回数・合計時間・平均時間・読んだブロック数などを記録してくれるため、遅いと感じたときに当てずっぽうでインデックスを足すのではなく、数字にもとづいて直す対象を選べます。この記事では、有効化の手順、重いクエリを探す集計の書き方、数字の読み方、改善後に効果を確かめる方法までを、作業の流れに沿って説明します。

WordPress 本体は MySQL / MariaDB を前提としており、PostgreSQL は公式にはサポートされていません(2026年10月現在)。ここでは WordPress と並べて運用する別システムや分析用データベースで PostgreSQL を使っている場面を想定しています。MySQL で同じことをしたい場合は、スロークエリログや Performance Schema が相当します。

有効化の手順

pg_stat_statements は PostgreSQL に付属する拡張機能(contrib)ですが、使うには共有ライブラリとしてサーバー起動時に読み込ませる必要があります。設定ファイルを変更する前に、元のファイルのコピーを取っておいてください。

# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'   # 既に他の値があればカンマで追加
pg_stat_statements.max = 5000      # 記録する SQL の種類の上限(既定 5000)
pg_stat_statements.track = top     # top: 直接実行された文のみ / all: 関数内の文も

shared_preload_libraries の変更は、PostgreSQL の再起動をしないと反映されません。リロードだけで済ませて「ビューが空のまま」「拡張機能を作るとエラーになる」と悩むことがよくあります。再起動はサービスが止まるため、メンテナンス時間に行います。

再起動後、統計を見たいデータベースで拡張機能を作成します。

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SHOW shared_preload_libraries;   -- pg_stat_statements が含まれているか
SELECT count(*) FROM pg_stat_statements;

統計自体はサーバー全体で集められ、dbid 列でデータベースごとに区別されます。拡張機能はビューを見るためのもので、どのデータベースに作っても全体の統計を参照できます。

Amazon RDS や Cloud SQL などのマネージドサービスでは、設定ファイルを直接編集せず、パラメーターグループやデータベースフラグで指定します。最初から読み込まれている場合もあるため、まず SHOW shared_preload_libraries; で現状を確認し、手順は各サービスのドキュメントに従ってください。

重いクエリを探す基本の集計

PostgreSQL 13 以降では、実行時間の列は total_exec_time や mean_exec_time という名前です(12 以前は total_time・mean_time)。単位はミリ秒です。まずは合計時間の大きい順に並べます。

SELECT queryid,
       calls,
       round(total_exec_time::numeric, 1)  AS total_ms,
       round(mean_exec_time::numeric, 2)   AS mean_ms,
       rows,
       round(100.0 * shared_blks_hit
             / nullif(shared_blks_hit + shared_blks_read, 0), 1) AS hit_pct,
       left(query, 100) AS query
FROM pg_stat_statements
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY total_exec_time DESC
LIMIT 10;

query 列の SQL は、WHERE id = 123 の 123 のような値が $1 などに置き換えられた形で記録されます。値だけが違う同じ形の SQL が1行にまとまるため、「1回は速いが何万回も呼ばれている SQL」が見えるようになります。

数字の読み方:どの順番で見るか

並べ替えの基準によって、見えてくる問題が違います。目的に合わせて ORDER BY を変えてください。

  • 合計時間(total_exec_time)の大きい順:サーバー全体の負荷への寄与が大きい SQL です。改善の効果がもっとも大きいのは、たいていここの上位です。
  • 平均時間(mean_exec_time)の大きい順:1回ごとに遅い SQL です。画面表示やバッチの待ち時間に直結します。ただし呼び出し回数が少ないものは、たまたま遅かっただけの可能性もあるので calls と合わせて見ます。
  • 呼び出し回数(calls)の大きい順:アプリケーション側のループで同じ SQL を何度も投げている(いわゆる N+1)状態を見つけやすくなります。SQL を速くするより、呼び出し方をまとめる方が効くことがあります。
  • 1回あたりの行数(rows / calls)が多いもの:必要以上の行を取得していないかを疑います。

あわせて、次の列も手がかりになります。

  • shared_blks_read が多く、上の hit_pct(共有バッファで見つかった割合)が低い:データがメモリに乗らずディスクから読んでいます。インデックスが使われず広い範囲を読んでいる可能性があります。
  • temp_blks_written が0より大きい:並べ替えやハッシュがメモリに収まらず、一時ファイルに書き出しています。インデックスで並べ替えを省けないか、対象行を減らせないかを先に検討し、それでも必要なら work_mem をその処理に限って上げることを考えます。
  • max_exec_time が平均よりずっと大きい:ロック待ちなど、SQL 自体以外の要因で遅くなったことがあるかもしれません。

1本の SQL を掘り下げる

直す対象が決まったら、query 列の $1 に実際に使われる値を入れて、EXPLAIN (ANALYZE, BUFFERS) で実行計画を確かめます。

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 42
ORDER BY ordered_at DESC
LIMIT 20;

EXPLAIN ANALYZE は SQL を実際に実行します。UPDATE や DELETE を調べるときは BEGIN; と ROLLBACK; で囲み、変更が残らないようにしてください。計画の中で Seq Scan(表の全件読み)に大きな時間がかかっていればインデックスの追加を、見積もり行数と実際の行数が大きくずれていれば ANALYZE による統計の更新を検討します。値によって計画が変わることもあるので、代表的な値を何種類か試すと見落としが減ります。

インデックスを追加するときは、本番では CREATE INDEX CONCURRENTLY を使うと、作成中も書き込みを止めずに済みます。作業前にバックアップを取り、検証環境で効果を確かめてから適用してください。

改善の効果を確かめる

改善後に同じ集計を見ても、過去の実行分が積み上がっているため、変化が分かりにくいことがあります。方法は二つあります。

一つは統計を一度リセットする方法です。

SELECT pg_stat_statements_reset();

既定ではスーパーユーザーだけが実行できます。リセットするとそれまでの記録は消えるため、ほかの人が調査に使っていないか確かめてから行います。

もう一つは、リセットせずにスナップショットを取って差分を見る方法です。

CREATE TABLE pss_before AS
SELECT queryid, dbid, userid, calls, total_exec_time
FROM pg_stat_statements;

-- 改善を適用して、しばらく運用した後
SELECT s.queryid,
       s.calls - b.calls AS calls_delta,
       round(((s.total_exec_time - b.total_exec_time)
              / nullif(s.calls - b.calls, 0))::numeric, 2) AS mean_ms_delta
FROM pg_stat_statements s
JOIN pss_before b USING (queryid, dbid, userid)
ORDER BY s.total_exec_time - b.total_exec_time DESC
LIMIT 10;

差分の期間は、平日と休日など負荷の傾向が近い時間帯どうしで比べると、改善の効果を読み違えにくくなります。

運用上の注意

  • 権限:一般ユーザーは、他のユーザーが実行した SQL の本文を見られません。監視用のロールには pg_read_all_stats を付与します。
  • 上限:記録できる SQL の種類は pg_stat_statements.max までで、超えると実行回数の少ないものから捨てられます。PostgreSQL 14 以降は pg_stat_statements_info ビューの dealloc 列で、捨てられた回数を確認できます。値が増え続けるなら上限を上げます(再起動が必要です)。
  • 負荷:記録には小さなオーバーヘッドがあります。計画作成の時間も記録する pg_stat_statements.track_planning は既定で無効で、有効にすると負荷が増えるため、必要なときだけ使います。
  • バージョン差:列の名前や追加された項目は、メジャーバージョンごとに変わることがあります。監視ツールやスクリプトを作るときは、使っているバージョンのドキュメントで列名を確認してください。

まとめ

  • shared_preload_libraries に追加して再起動し、CREATE EXTENSION で有効にします。マネージドサービスでは設定方法が異なります。
  • 合計時間・平均時間・呼び出し回数のそれぞれで並べ替え、負荷の大きい SQL、1回が遅い SQL、回数が多すぎる SQL を見分けます。
  • shared_blks_read や temp_blks_written は、ディスク読み込みやメモリ不足の手がかりになります。
  • 対象を決めたら実際の値で EXPLAIN (ANALYZE, BUFFERS) を取り、改善後はリセットかスナップショットの差分で効果を確かめます。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲