PostgreSQL 13 min read

PostgreSQLの実行計画の見方 ― EXPLAINとEXPLAIN ANALYZE、推定行数と実際の行数のずれ

※この記事にはアフィリエイト広告(プロモーション)が含まれます。

SQL が遅いとき、PostgreSQL がどう実行するつもりかは EXPLAIN で、実際にどう実行したかは EXPLAIN ANALYZE で見ます。この記事の出力は PostgreSQL 18.6(Ubuntu 26.04)で、30万行の orders 表を使って採りました。

まず見る(結論)

EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
  • Seq Scan は表を全部読んでいる。Rows Removed by Filter が大きければ、索引で絞れていない
  • rows= が2つ出る。左の (cost=... rows=30) が推定、右の (actual ... rows=30.00) が実際。桁がずれていたら統計が古い
  • EXPLAIN ANALYZE は SQL を本当に実行する。UPDATE / DELETE は BEGIN 〜 ROLLBACK で囲む

EXPLAIN と EXPLAIN ANALYZE

EXPLAIN は計画だけを表示し、SQL は実行しません。

EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
                                QUERY PLAN
--------------------------------------------------------------------------
 Gather  (cost=1000.00..5414.88 rows=30 width=30)
   Workers Planned: 1
   ->  Parallel Seq Scan on orders  (cost=0.00..4411.88 rows=18 width=30)
         Filter: (customer_id = 42)

EXPLAIN ANALYZE は実際に実行し、かかった時間と行数を加えて表示します。

EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..5414.88 rows=30 width=30) (actual time=0.464..9.080 rows=30.00 loops=1)
   Workers Planned: 1
   Workers Launched: 1
   Buffers: shared hit=2206
   ->  Parallel Seq Scan on orders  (cost=0.00..4411.88 rows=18 width=30) (actual time=0.147..5.651 rows=15.00 loops=2)
         Filter: (customer_id = 42)
         Rows Removed by Filter: 149985
         Buffers: shared hit=2206
 Planning Time: 0.433 ms
 Execution Time: 9.121 ms
項目 意味
cost=0.00..4411.88 推定コスト(最初の1行まで..全部まで)。単位は時間ではない
rows=18(cost 側) 推定行数
actual time=0.147..5.651 実際の時間(ミリ秒)
rows=15.00 loops=2 実際の行数。1回あたりの値なので、合計は rows × loops(ここでは 30 行)
Rows Removed by Filter 読んだが条件に合わず捨てた行数
Buffers: shared hit 読んだページ数(hit はメモリ上にあった分、read はディスクから読んだ分)
Execution Time 実行にかかった時間の合計

30行を返すために約30万行を読んで捨てています。Gather と Parallel Seq Scan は並列実行で、本体とワーカー1つで分けて読んでいます。

PostgreSQL 18 では EXPLAIN ANALYZE と書くだけで Buffers 行が出ます。消すには EXPLAIN (ANALYZE, BUFFERS OFF) と書きます。時間やコストを省いて計画の形だけを見たいときは EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) も使えます。

読み取り方の種類

customer_id に索引を作ってから同じ SQL を流しました。

 Bitmap Heap Scan on orders  (cost=4.53..114.41 rows=30 width=30) (actual time=0.034..0.173 rows=30.00 loops=1)
   Recheck Cond: (customer_id = 42)
   Heap Blocks: exact=30
   ->  Bitmap Index Scan on idx_orders_customer  (cost=0.00..4.52 rows=30 width=0) (actual time=0.022..0.022 rows=30.00 loops=1)
         Index Cond: (customer_id = 42)
 Execution Time: 0.216 ms

9.121 ms が 0.216 ms になりました。Index Cond は索引で絞った条件、Filter は読んでから捨てた条件です。

表示 読み方 出た場面(この検証)
Seq Scan 表を先頭から全部読む 索引が無い列での絞り込み、表の9割が当たる条件
Index Scan 索引をたどって該当行だけ読む 一意な値の検索、時刻の範囲検索
Bitmap Heap Scan 索引で該当ページを集めてからまとめて読む 30行〜3万行が当たる条件
Index Scan Backward 索引を逆順にたどる ORDER BY ... DESC LIMIT

索引があっても、大半の行を返す条件では全部読むほうが速いと判断されて Seq Scan になります。

結合・集計・並べ替え

EXPLAIN ANALYZE SELECT c.name, o.amount FROM orders o JOIN customers c ON c.id = o.customer_id
 WHERE c.name = 'cust42';
 Gather  (cost=1208.01..5645.16 rows=30 width=14) (actual time=1.434..16.664 rows=30.00 loops=1)
   ->  Hash Join  (cost=208.01..4642.16 rows=18 width=14) (actual time=0.788..12.677 rows=15.00 loops=2)
         Hash Cond: (o.customer_id = c.id)
         ->  Parallel Seq Scan on orders o  (...) (actual time=0.005..6.207 rows=150000.00 loops=2)
         ->  Hash  (...) (actual time=0.394..0.395 rows=1.00 loops=2)
               ->  Seq Scan on customers c  (...) (actual time=0.011..0.389 rows=1.00 loops=2)
                     Filter: (name = 'cust42'::text)
 Execution Time: 16.716 ms

字下げが深い行から先に実行されます。customers から1行を探してハッシュ表にし、orders を全部読みながら突き合わせています。時間がかかっているのは orders の全件読みです。

推定行数と実際の行数がずれる

自動 ANALYZE を止めた表に、status = 'hold' の行を5万件追加し、その直後に流しました。

EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'hold';
 Gather  (cost=1000.00..6147.96 rows=1 width=30) (actual time=8.172..13.429 rows=50000.00 loops=1)
   ->  Parallel Seq Scan on orders  (cost=0.00..5147.86 rows=1 width=30) (actual time=6.626..8.352 rows=25000.00 loops=2)

推定 1 行に対して実際は 50000 行です。統計にまだ hold が無いためで、pg_stat_user_tables の n_mod_since_analyze(前回の ANALYZE 以降に変わった行数)は 50002 でした。

ANALYZE orders;
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'hold';
 Seq Scan on orders  (cost=0.00..6949.00 rows=50365 width=30) (actual time=12.714..15.497 rows=50000.00 loops=1)

推定がずれると、結合の方法や索引を使うかどうかの判断を誤ります。大量に投入・削除した直後に遅くなったら、先に ANALYZE を流して計画が変わるかを見ます。

EXPLAIN ANALYZE は更新を実行する

SELECT amount FROM orders WHERE id = 1;            -- 1.50
EXPLAIN ANALYZE UPDATE orders SET amount = amount + 100 WHERE id = 1;
SELECT amount FROM orders WHERE id = 1;            -- 101.50

更新文の実際の動きを見るときはトランザクションで囲みます。

BEGIN;
EXPLAIN ANALYZE UPDATE orders SET amount = amount + 100 WHERE id = 1;
ROLLBACK;

この形では ROLLBACK の後も 101.50 のままでした。

JSON で出す

EXPLAIN (FORMAT JSON) は "Node Type": "Seq Scan"、"Plan Rows": 18 のような形で出ます。可視化ツールに貼るときや、プログラムで読むときに使います。

関連: PostgreSQLのインデックス作成(CREATE INDEX) / PostgreSQLのVACUUM / MySQLのEXPLAINの見方

PostgreSQL をもっと深く知るための本

PostgreSQL実践入門 ──アーキテクチャ、運用監視、性能改善

技術評論社・2026年2月

アーキテクチャから運用監視・性能改善までを扱う。付録に PostgreSQL 18 の新機能と設定項目の差分がある。

Amazonで見る

[改訂3版]内部構造から学ぶPostgreSQL ―設計・運用計画の鉄則

技術評論社・2022年11月・PostgreSQL 14 ベース

内部構造の理解から、設計と運用計画の立て方を学ぶ。

Amazonで見る

Amazonのアソシエイトとして、FIRE SE は適格販売により収入を得ています。