PostgreSQL を使ったサービスで「最近アプリが遅い」「DB の CPU 使用率が高い」と言われたとき、まず知りたいのはどのクエリが原因かです。この記事では PostgreSQL で遅いクエリを見つける方法を、pg_stat_statements、ログ、auto_explain、pg_stat_activity の 4 つに分けて説明します。それぞれ答えられる問いが違うので、目的に合わせて使い分けるのが近道です。
PostgreSQL で遅いクエリを調べる 4 つの方法
最初に全体像を示します。
| 知りたいこと | 使うもの | 既定の状態 |
|---|---|---|
| いま実行中で、長く止まっているクエリはどれか | pg_stat_activity ビュー |
使える |
| 普段から DB の時間を多く使っているクエリはどれか | pg_stat_statements 拡張 |
要設定 (再起動が必要) |
| 何時何分に、どのクエリが何秒かかったか | log_min_duration_statement |
無効 |
| 遅かったクエリが、そのときどんな実行計画だったか | auto_explain モジュール |
無効 |
障害の最中なら pg_stat_activity、改善の対象を選ぶなら pg_stat_statements、あとから時刻を追うならログ、原因を詰めるなら auto_explain という使い分けになります。以下、順に見ていきます。
いま遅いクエリを pg_stat_activity で確かめる
画面が固まっている、バッチが終わらないといった「いま起きている」問題なら、まず実行中のクエリを見ます。pg_stat_activity は接続ごとに 1 行を返すビューで、追加の設定なしで使えます。
SELECT pid,
usename,
state,
now() - query_start AS running_for,
wait_event_type,
wait_event,
left(query, 100) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
AND pid <> pg_backend_pid()
ORDER BY query_start;
running_for が長いものが長く実行されているクエリです。wait_event_type が Lock なら、ほかのトランザクションのロックを待っています。その場合は遅いクエリそのものより、ロックを持ったままのトランザクションを探すほうが先です。ロック待ちの調べ方は ロック待ちの記事 で説明しています。
state が idle in transaction の接続にも注意してください。トランザクションを開いたまま何もしていない状態で、ロックを持ち続けたり、VACUUM が古い行を片付けるのを妨げたりします。
止める必要があるときは、まず pg_cancel_backend(pid) で実行中のクエリだけを取り消します。それでも止まらないときに、接続ごと切る pg_terminate_backend(pid) を使います。
pg_stat_statements で重いクエリを探す
pg_stat_statements は、実行されたクエリを型ごとにまとめ、実行回数や合計時間を集計する拡張機能です。PostgreSQL に同梱されていますが、使うには設定が要ります。日々の改善の対象を選ぶには、これがいちばん役に立ちます。
有効にする
pg_stat_statements は共有メモリを使うため、shared_preload_libraries に入れてサーバーを再起動する必要があります。
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
compute_query_id = auto # 既定値。off だと集計されない
pg_stat_statements.max = 10000 # 既定は 5000
pg_stat_statements.track = top # 既定。関数の中の文も見るなら all
再起動したら、ビューを使いたいデータベースで拡張を作ります。
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SHOW shared_preload_libraries;
CREATE EXTENSION が成功しても、shared_preload_libraries に入っていなければビューを読むときにエラーになります。SHOW の結果に pg_stat_statements が含まれているかを確かめてください。
Amazon RDS for PostgreSQL (バージョン 10 以降) と Aurora PostgreSQL では、shared_preload_libraries に既定で入っています。この場合は CREATE EXTENSION だけで使い始められます。
ほかのユーザーが実行したクエリの文と queryid を見られるのは、スーパーユーザーと pg_read_all_stats ロールを持つユーザーだけです。監視用のユーザーで見る場合は、このロールを付けておきます。
合計時間の多いクエリを出す
最初に見るべきは合計時間 (total_exec_time) の多い順です。
SELECT queryid,
calls,
round(total_exec_time::numeric, 0) AS total_ms,
round(mean_exec_time::numeric, 1) AS mean_ms,
rows,
round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 1) AS pct,
left(query, 100) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
時間の単位はミリ秒です。pct は、集計対象のすべてのクエリの合計時間に占める割合です。上位の数件で全体の大半を占めることがよくあり、そうした数件を直すと DB 全体の負荷が目に見えて下がります。
ここで、1 回が遅いクエリと、1 回は速いが回数が多いクエリを分けて見てください。mean_ms が 2,000 で calls が 50 のクエリと、mean_ms が 3 で calls が 300 万のクエリは、どちらも合計は大きくなります。前者はクエリそのもの (インデックスや書き方) を直します。後者は、アプリが同じクエリを繰り返し呼んでいないか (N+1 問題など) を疑います。後者の見つけ方と直し方は N+1 問題の記事 にまとめています。
PostgreSQL 12 以前では、列名が total_time、mean_time です。13 以降は実行時間と計画時間が分かれ、total_exec_time、mean_exec_time になりました。
ディスクからの読み込みが多いクエリを出す
時間だけでなく、どこから読んでいるかも手がかりになります。shared_blks_hit はメモリ上の共有バッファで見つかったブロック数、shared_blks_read はそこになく読み込んだブロック数です。
SELECT queryid,
calls,
shared_blks_hit,
shared_blks_read,
round(100.0 * shared_blks_hit
/ nullif(shared_blks_hit + shared_blks_read, 0), 1) AS hit_pct,
temp_blks_written,
left(query, 100) AS query
FROM pg_stat_statements
ORDER BY shared_blks_read DESC
LIMIT 10;
shared_blks_read が多いクエリは大きなテーブルを広く読んでいる可能性があります。temp_blks_written が 0 でないクエリは、ソートやハッシュがメモリに収まらず一時ファイルを書いています。work_mem が足りないか、そもそも扱う行が多すぎるかのどちらかです。
読み込みにかかった時間も知りたい場合は、track_io_timing を有効にすると、I/O の時間の列 (PostgreSQL 17 以降は shared_blk_read_time など) に値が入ります。
表示される SQL の読み方
pg_stat_statements の query 列では、定数が $1、$2 のような記号に置き換えられています。WHERE user_id = 42 と WHERE user_id = 7 は同じ型として 1 行にまとまります。
このため、ここに出た SQL をそのまま EXPLAIN にかけることはできません。実際の値はアプリのコードやログから拾ってきます。後で説明するログや auto_explain を併用すると、実際の値と実行計画が手に入ります。
統計の注意点
pg_stat_statements の数字は最後にリセットしてからの累積です。改善の効果を測りたいときは、改善の前にリセットし、しばらく待ってから見直します。
-- すべての統計を消す
SELECT pg_stat_statements_reset();
-- 最後にリセットした時刻と、上限を超えて捨てられた回数
SELECT stats_reset, dealloc FROM pg_stat_statements_info;
pg_stat_statements_info は PostgreSQL 14 以降で使えます。dealloc が増え続けているなら、記録できる型の数 (pg_stat_statements.max) が足りず、実行回数の少ない型から捨てられています。上限を上げるには再起動が必要です。
ログで遅いクエリを記録する (log_min_duration_statement)
pg_stat_statements は累積の数字なので、「昨日の 14 時台に何が遅かったか」には答えられません。時刻つきで残したいときは、log_min_duration_statement で、しきい値を超えたクエリをサーバーログに書き出します。既定は -1 (無効) です。
設定する
単位を書かなければミリ秒として扱われます。再起動は要らず、設定の再読み込みで反映されます。
ALTER SYSTEM SET log_min_duration_statement = '500ms';
SELECT pg_reload_conf();
SHOW log_min_duration_statement;
データベースやユーザーごとに変えることもできます。この場合は、変更後に作られた接続から使われます。
-- 管理画面用のデータベースだけ、しきい値を長めにする
ALTER DATABASE admin_app SET log_min_duration_statement = '2s';
しきい値の決め方
最初は 500 ミリ秒から 1 秒程度にして、ログの量を見ながら調整するのが無難です。0 にするとすべてのクエリが記録され、ログの書き込みそのものが負荷になります。
すべてを記録したいが量を抑えたい場合は、log_min_duration_sample と log_statement_sample_rate で一部だけを抜き出して記録できます。たとえば log_min_duration_sample = 100ms、log_statement_sample_rate = 0.1 とすると、100 ミリ秒以上のクエリのうちおよそ 1 割が記録されます。log_min_duration_statement を超えたクエリは抜き出しとは関係なくすべて記録されます。
ログの読み方
記録は次のような形になります (値は例です)。
2026-10-28 14:12:03.512 JST [48211] LOG: duration: 1834.207 ms execute <unnamed>: SELECT id, total FROM orders WHERE customer_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT 20
2026-10-28 14:12:03.512 JST [48211] DETAIL: Parameters: $1 = '81723', $2 = 'paid'
duration が実行時間、[48211] がプロセス ID です。アプリがプリペアドステートメントを使っている場合は、DETAIL の行にバインド変数の実際の値が出ます。この値を SQL に当てはめれば、EXPLAIN で再現できます。
既定のログの行頭 (log_line_prefix) は時刻とプロセス ID だけです。誰がどのデータベースで実行したかも追いたいなら、次のように足しておくと便利です。
log_line_prefix = '%m [%p] %q%u@%d/%a '
ログに個人情報が残る点に注意
ログには SQL の文とバインド変数がそのまま書かれます。メールアドレスや電話番号で検索するクエリがあれば、その値もログに残ります。ログを外部のサービスに送っている場合は特に気をつけてください。バインド変数を記録したくない場合は log_parameter_max_length = 0 にします。
auto_explain で遅いクエリの実行計画を残す
ログで遅いクエリが分かっても、手元で EXPLAIN を打つと速いことがあります。データの分布やキャッシュの状態が、遅かったときと違うからです。auto_explain を使うと、しきい値を超えたクエリについて、実行されたときの実行計画をログに残せます。
設定する
# postgresql.conf
session_preload_libraries = 'auto_explain'
auto_explain.log_min_duration = '1s'
auto_explain.log_min_duration の既定は -1 (無効) なので、設定しないと何も記録されません。session_preload_libraries なら新しい接続から読み込まれ、再起動は要りません。試すだけなら、スーパーユーザーで接続して LOAD 'auto_explain'; を実行すれば、その接続の中だけで使えます。
log_analyze の負荷に注意
auto_explain.log_analyze = on にすると、見積もりだけでなく実際の行数や時間 (EXPLAIN ANALYZE 相当) が記録され、原因を詰めやすくなります。ただし公式ドキュメントでは、この設定を有効にすると、記録されるかどうかに関係なく、すべての文で計画のノードごとの時間を測るため、性能が大きく落ちることがあると警告しています。
本番で使う場合は、auto_explain.log_timing = off にしてノードごとの時間の計測を止めるか、auto_explain.sample_rate で対象の接続を絞るのが現実的です。log_timing を切っても、実際の行数は記録されます。見積もりと実際の行数のずれは、それだけで原因の手がかりになります。
記録された実行計画の読み方は PostgreSQL の EXPLAIN の記事 で詳しく説明しています。
RDS・Aurora・Cloud SQL での確認
マネージドサービスでは postgresql.conf の代わりにパラメータグループやデータベースフラグで設定します。
Amazon RDS for PostgreSQL と Aurora PostgreSQL では、DB パラメータグループで log_min_duration_statement や auto_explain の設定を変えます。ログはコンソールからダウンロードできるほか、CloudWatch Logs に出力しておけば、CloudWatch Logs Insights で時間帯や文字列で絞り込めます。クエリごとの負荷は CloudWatch Database Insights (以前の Performance Insights) の画面でも見られます。
Google Cloud の Cloud SQL for PostgreSQL では、データベースフラグでログの設定を変えます。また Query Insights を有効にすると、合計実行時間の多いクエリの一覧、負荷の内訳 (CPU、I/O 待ち、ロック待ち)、抜き出した実行計画を画面で確かめられます。指標の保持期間は Enterprise エディションで 7 日、Enterprise Plus エディションで 30 日です (2026 年 11 月時点、Google Cloud 公式ドキュメントより)。
遅いクエリを見つけたあとにやること
見つけたクエリは pg_stat_statements の合計時間の多いものから手をつけます。そのうえで次の順に確かめると、原因に早くたどり着けます。
EXPLAIN (ANALYZE, BUFFERS)で実行計画を取り、大きなテーブルをSeq Scanで読んでいないか、見積もりの行数と実際の行数が大きくずれていないかを見る。- ずれが大きければ
ANALYZE テーブル名;で統計情報を更新し、計画が変わるかを確かめる。 - 条件の列にインデックスがなければ追加する。本番で大きなテーブルに作るときは、書き込みを止めない
CREATE INDEX CONCURRENTLYを使う。 - 回数の多いクエリは、アプリの呼び方 (ループの中の問い合わせ) を見直す。
改善したら pg_stat_statements で同じ queryid の mean_exec_time を比べ、効果を確かめます。
この確認を続けるには
遅いクエリは、データが増えたり新しい機能が入ったりすると入れ替わります。手作業で続けるなら、週に一度 pg_stat_statements の上位 10 件を見て、前週との違いを確かめるのが手軽です。cron で集計結果をチームのチャットに送ったり、監視ツールでログの件数にアラートをかけたりする方法もあります。
AI に任せる方法もあります。Scandock は AI がデータベースを監視するサービスです。PostgreSQL などの接続文字列を登録すると、AI がテーブルの構成と実データを読んで何を監視するかを決めます。固定の閾値ではなく値の推移や過去の経緯を踏まえて異常かどうかを判断し、異常があれば読み取り専用のクエリで関連するデータを調べて、原因・影響・根拠・対処案をつけて通知します。Amazon RDS、Cloud SQL、Neon にも対応しています。
参考
- PostgreSQL 18 Documentation: pg_stat_statements
- PostgreSQL 18 Documentation: Error Reporting and Logging
- PostgreSQL 18 Documentation: auto_explain
- PostgreSQL 18 Documentation: The Cumulative Statistics System (pg_stat_activity)
- Amazon RDS ユーザーガイド: Common DBA tasks for Amazon RDS for PostgreSQL
- Amazon Aurora ユーザーガイド: SQL statistics for Aurora PostgreSQL
- Cloud SQL for PostgreSQL: Use Query insights to improve query performance