مرکزی مواد پر جائیں
友田 陽大
PostgreSQL internals & performance
PostgreSQL
パフォーマンス
アーキテクチャ設計

PostgreSQL EXPLAIN ANALYZE の読み方とスロークエリ改善(実行計画の見方・ノード別の意味・auto_explain・v18対応)

PostgreSQL の遅いクエリを EXPLAIN ANALYZE で診断し、確実に速くするための実務ガイド。cost/rows/width の読み方、見積もりと実測のズレ(統計の陳腐化)、Seq Scan・Bitmap・Nested Loop など各ノードの意味、外部ソートや Heap Fetches などの警告サイン、本番で遅いクエリの計画をログに残す auto_explain、PostgreSQL 18 の BUFFERS 既定ON までを公式ドキュメントに忠実に解説します。

Published
Reading time
11 min read
Author
友田 陽大
شیئر کریں

「クエリが遅い」を直すのに、勘は要りません。PostgreSQL は EXPLAIN で実行計画を、EXPLAIN ANALYZE で実測を見せてくれます。読めるようになれば、「なぜ遅いか」が一目で分かり、打ち手(索引・統計更新・クエリ書き換え・メモリ)が確定します。

この記事は、パフォーマンス総論の §2 で特定した「重いクエリ」を、実際に診断して直すための読み方ガイドです。公式ドキュメントの定義に忠実に、現場で見る警告サインと対処まで落とし込みます。

この記事のルールEXPLAIN の出力項目の意味・各オプション・PostgreSQL 18 の変更点は、すべて PostgreSQL 18 公式ドキュメント(2026年6月時点) に基づきます。コストは「ディスクページ取得を1.0とした任意単位」で、絶対値ではなくプラン間の相対比較に使うものです。


1. 4つの数字を読む:cost / rows / width

まず素の EXPLAIN(実行しない=見積もりのみ)。

EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
Index Scan using idx_orders_cust on orders  (cost=0.42..8.45 rows=3 width=72)
  Index Cond: (customer_id = 42)

括弧内の4つの数字が基本です(公式 using-explain)。

項目意味
cost=0.42..8.45開始コスト..総コスト0.42=最初の行を返せるまで、8.45=全行返し終えるまで
rows=3このノードが出力すると見積もった行数
width=721行あたりの推定平均バイト数

ポイント。

  • 開始コストは「出力開始までの準備」(ソートノードならソート完了まで)。
  • 総コストは「最後まで読み切る前提」。親が LIMIT で途中で打ち切れば、実際はここまで掛かりません。
  • コストは累積。親ノードのコストは子ノードを含む。
  • 単位は任意(seq_page_cost = 1.0 基準)。絶対値に意味はなく、プラン同士の比較に使います。

PostgreSQL 18 の小ネタEXPLAINrows小数表示になり得ます(従来は整数に丸め)。微小な選択率がより正確に見えます。


2. EXPLAIN ANALYZE:実測を出す(DMLは要注意)

ANALYZE を付けると、PostgreSQL は実際にクエリを実行し、見積もりと実測を並べます。

EXPLAIN (ANALYZE) SELECT * FROM orders WHERE customer_id = 42;
Index Scan using idx_orders_cust on orders
  (cost=0.42..8.45 rows=3 width=72) (actual time=0.018..0.021 rows=3 loops=1)
  Index Cond: (customer_id = 42)
  Buffers: shared hit=4
Planning Time: 0.10 ms
Execution Time: 0.04 ms

actual time=0.018..0.021 rows=3 loops=1 が実測です。(cost=…) (actual=…) を左右で見比べるのが診断の基本動作。

危険:ANALYZE は本当に実行する。公式の警告どおり、INSERT/UPDATE/DELETE/MERGEEXPLAIN ANALYZE を付けるとデータが変わりますSELECT の返却は破棄されますが、副作用は起きる。DML を計測するときは必ずトランザクションで囲んでロールバックします。

BEGIN;
EXPLAIN (ANALYZE) UPDATE orders SET status = 'shipped' WHERE id = 42;
ROLLBACK;   -- 計画は分かったが、変更は取り消す

3. 最重要:見積もり rows と実測 rows のズレ

公式が「最も重要」と明言するのは——見積もり行数が実測にどれだけ近いか

Seq Scan on orders
  (cost=0.00..18334.00 rows=1 width=72) (actual ... rows=48210 loops=1)
                       ^^^^^^                            ^^^^^^^^^^^
                       見積もり1行              実測は48,210行(桁違い!)

このように桁違いにズレているとき、プランナーは間違った前提で計画を立てています。原因はほぼ統計情報の陳腐化ANALYZE で統計を取り直すと、多くの場合この種の問題は解決します。

ANALYZE orders;   -- 統計を更新してから、もう一度 EXPLAIN ANALYZE

それでもズレるなら、相関する複数列(プランナーは独立と仮定する)が原因のことが多い。CREATE STATISTICS で拡張統計を与えます(総論 §3 参照)。見積もりが正しくなれば、プランナーは自然と良い計画を選びます。「索引を強制する」より「統計を正す」が王道です。


4. loops の罠:実測値は「1ループあたりの平均」

ここを誤読する人が非常に多い。公式の定義:

サブプランノードが複数回実行される場合、loops総実行回数を表し、actual timerows1回あたりの平均で表示される。総時間を知るには loops を掛ける

つまり actual time=0.5 rows=2 loops=10000 は「平均0.5ms × 1万回 = 合計5秒」。Nested Loop の内側で起きると、見た目の小さな数字が総時間では爆発します。

Nested Loop  (actual time=... rows=10000 loops=1)
  ->  Seq Scan on a   (actual ... rows=10000 loops=1)
  ->  Index Scan on b (actual time=0.05..0.06 rows=1 loops=10000)
                                                        ^^^^^^^^^^
                          内側が1万回実行 → 0.06ms × 10000 ≈ 0.6秒がここに隠れている

外側の行数が多いのに Nested Loop が選ばれているなら、行数の見積もり誤りでプランナーが Hash/Merge 結合を選び損ねている可能性大。やはり統計を疑います。


5. ノードの意味:何が選ばれたら何を意味するか

実行計画は木構造で、葉がスキャン、枝が結合・集約です。主要ノードの意味と「出たら何を考えるか」。

スキャン系

ノード意味出たときの考え
Seq Scan全行を順に読む大量行を返すなら正常。少数を返すのに出たら索引不足を疑う
Index Scan索引で位置特定→ヒープ取得選択的な条件・整列に最適。理想形の一つ
Index Only Scan索引だけで完結(ヒープ不要)最速級。Heap Fetches を確認(索引の記事
Bitmap Index Scan → Bitmap Heap Scan位置をビットマップに集めてから物理順でヒープ取得中程度の選択率。Seq と Index の中間で効率的

結合系

ノード意味得意な状況
Nested Loop外側1行ごとに内側を再スキャン外側が少数のとき。多いと爆発(§4)
Hash Join片方からハッシュ表を作り突き合わせ大きい非整列の等値結合
Merge Join両側を整列してマージ両側が既に整列済みの大規模結合
-- VERBOSE で出力列・スキーマ修飾名まで、FORMAT json で機械可読に出せる
EXPLAIN (ANALYZE, VERBOSE, FORMAT JSON) SELECT ...;

6. 警告サイン集:これが出たらこう直す

実行計画から「遅い理由」を読み取る、現場のパターン集です。

Rows Removed by Filter が巨大 → 索引不足

Seq Scan on orders  (actual ... rows=120 loops=1)
  Filter: (status = 'pending')
  Rows Removed by Filter: 1488000   ← 149万行を読んで120行に絞った(無駄が大きい)

149万行を読んでフィルタで捨てている。status に索引(または部分索引)を張れば、読む行を最初から絞れます。

Sort Method: external merge Disk: … → work_mem 不足

Sort  (actual time=820..950 rows=500000 loops=1)
  Sort Key: created_at
  Sort Method: external merge  Disk: 86016kB   ← メモリに収まらずディスクへ退避

ソートが work_mem を溢れてディスクに書いた証拠。そのクエリだけ SET LOCAL work_mem を上げるか(総論 §4.2)、ORDER BY を満たす索引を用意してソート自体を消します。Memory: NkB(quicksort)ならメモリ内で完結=健全。

Heap Fetches が多い → VACUUM 不足

Index Only Scan using idx_orders_cust_covering on orders
  (actual ...) Heap Fetches: 43120   ← index-only のはずがヒープを4万回も見ている

可視性マップが all-visible でないため、index-only scan がヒープを見に行っている。対象表を VACUUM すれば Heap Fetches は下がります(MVCC/VACUUM の記事)。

④ Nested Loop で内側の loops が巨大 → 結合方式の誤選択

§4 のとおり。多くは見積もり誤り。ANALYZE/拡張統計で見積もりを正すのが本筋です。


7. BUFFERS でキャッシュ効率を読む(PG18で既定ON)

BUFFERS は、各ノードが触れたバッファ数を出します。メモリに乗っているか、ディスクを叩いているかが分かります。

項目意味
shared hitキャッシュ命中(ディスク読み回避)
shared readディスクから読んだ(コールド)
shared dirtiedこのクエリが変更した未変更ブロック
shared writtenこのクエリ実行中に追い出された dirty ブロック

PostgreSQL 18 の変更EXPLAIN ANALYZEBUFFERS を自動で含むようになりました(公式:「Buffers information is automatically included when ANALYZE is used.」)。PG17 以前は明示が必要でした。抑制したいときだけ EXPLAIN (ANALYZE, BUFFERS OFF)

Index Scan ... Buffers: shared hit=4          ← 全部キャッシュ命中(理想)
Seq Scan   ... Buffers: shared read=18334     ← 1.8万ブロックをディスクから読んだ(重い)

同じクエリを2回実行して readhit に変われば、2回目はキャッシュに乗った証拠。初回(コールド)と2回目(ウォーム)を区別して評価しましょう。PG18 ではこのほか、EXPLAIN ANALYZEインデックススキャンごとの索引探索回数を報告し、Material/Window Aggregate/CTE ノードのメモリ・ディスク使用量も出るようになりました。


8. 本番のスロークエリを捕まえる:auto_explain

開発中は EXPLAIN ANALYZE を手で叩けますが、本番では「いつ・どのクエリが遅かったか」を後から追う必要があります。そのための拡張が auto_explain——しきい値を超えたクエリの実行計画を自動でログに残します

# postgresql.conf(shared_preload_libraries 推奨)
shared_preload_libraries = 'auto_explain'
auto_explain.log_min_duration = '500ms'   # 500ms 超のクエリの計画をログ(-1=無効, 0=全件)
auto_explain.log_format = 'json'          # 構造化ログとして集約しやすい
auto_explain.sample_rate = 1.0            # 高トラフィックでは下げて負荷を抑える

最重要の注意(公式が強く警告):auto_explain.log_analyze = on にすると EXPLAIN ANALYZE 相当になり計画だけでなく実測も取れますが、「ログ対象でない短いクエリも含め全クエリでノード単位の計時が走り、性能に極めて悪影響を与え得る」。本番で常時ONは避け、必要なときだけ・log_timing = offsample_rate < 1 と併用して負荷を抑えます。

パラメータ既定役割
log_min_duration-1(無効)この時間(ms)を超えたら計画を記録。0 で全件
log_analyzeoff実測も取る。高負荷注意
log_timingonノード単位の計時(log_analyze 前提)。offで負荷軽減
log_buffersoffBUFFERS を含める(log_analyze 前提)
sample_rate1記録するクエリの割合。高負荷時に下げる

9. 診断〜改善のワークフロー(まとめ)

  1. 特定pg_stat_statements で累計時間の長いクエリを選ぶ(総論 §2)。
  2. 計測EXPLAIN (ANALYZE) を実行(DML は BEGIN; … ROLLBACK;)。
  3. 見積もりズレを確認rows 見積もり vs 実測。桁違いなら ANALYZE → 拡張統計
  4. 重いノードを特定actual time(× loops)が大きいノード、Rows Removed by Filterexternal merge DiskHeap Fetches
  5. 打ち手を確定
    • 大量フィルタ → 索引索引の記事
    • 外部ソート → work_mem or 整列索引
    • Heap Fetches → VACUUM
    • 結合誤選択 → 統計の修正
  6. 再計測:同じ EXPLAIN ANALYZE で改善を数値で確認(コールド/ウォームに注意)。
  7. 本番監視auto_explain で再発を検知。

やってはいけないSET enable_seqscan = off 等でプランナーを力技で曲げるのは、原因を隠す対症療法。デバッグ時の切り分けには使えますが、本番常用は禁物です。正しい統計と索引で、プランナーに正しい選択をさせるのが本筋。


10. まとめ

  • EXPLAIN は見積もり、EXPLAIN ANALYZE は実測。両者を左右で見比べる
  • 見積もりと実測のズレが最重要シグナル。桁違いなら統計の問題(ANALYZE/拡張統計)。
  • actualloops あたりの平均。総時間は loops 倍——Nested Loop の罠に注意。
  • 警告サイン(Rows Removed by Filter / external merge Disk / Heap Fetches)ごとに打ち手は決まっている。
  • PG18 は EXPLAIN ANALYZE が BUFFERS 既定ON。本番は auto_explain で計画を記録(log_analyze 常時ONは高負荷)。

実行計画が読めれば、チューニングは「勘」から「診断」になります。次は、計画を左右する物理レイヤー——MVCC とトランザクション、VACUUM へ。


参考(PostgreSQL 18 公式ドキュメント)

友田

友田 陽大

وزیر (METI) ایوارڈ یافتہ پروڈکٹ کے ڈویلپر۔ TypeScript + Python + AWS کے ساتھ میں SaaS، صنعتی ڈیجیٹل تبدیلی اور پروڈکشن کے قابل جنریٹو اے آئی (RAG) تنہا ابتدا سے انتہا تک فراہم کرتا ہوں — تقاضوں کی تعیین سے انفراسٹرکچر اور آپریشن تک۔

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

پروجیکٹ کی بنیاد پر کام اور تکنیکی مشاورت دونوں کے لیے دستیاب ہوں۔ 30 منٹ کی مفت مشاورت سے آغاز کریں۔

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

یہ بھی پڑھنے کے قابل