EXPLAIN と EXPLAIN ANALYZE の違い(loops は 1 回あたりの平均)
EXPLAIN は見積りだけを返すが、EXPLAIN ANALYZE は実際にクエリを実行して実測値も返す。actual time は「初行まで / 全行まで」の 2 つで、loops が 1 より大きいノードでは 1 回あたりの平均が表示される。総量に戻すには loops を掛ける。
見積りと実測を並べて出す
EXPLAIN は見積りだけを返します。EXPLAIN (ANALYZE) を付けると実際にクエリを実行して、 実測値も一緒に返します。
EXPLAIN (ANALYZE) SELECT ...;
すると各ノードにカッコが 2 つ並びます。
(cost=0.43..266.09 rows=23 width=12) (actual time=0.005..0.005 rows=2.00 loops=250000)
- 前半(
cost=…)が見積り - 後半(
actual …)が実測
この 2 つを見比べるのが EXPLAIN ANALYZE の主目的です。見積りが実測から大きく外れていれば、そのノードを起点に計画が壊れています。
ANALYZE は本当に実行します。UPDATE / DELETE / INSERT に付けるとデータが変わります。試すならBEGIN; … ROLLBACK; で囲みます。
actual time は 2 つある
actual time=0.005..0.005 の左は最初の 1 行まで、 右は全部返すまでです。cost の 2 つと対応しています。
Sort のように全部読まないと 1 行も返せないノードは、 左が大きくなります。逆に Seq Scan は 1 行目をすぐ返せるので左が小さいままです。
loops があると、表示は 1 回あたりの平均になる
ここがこのページでいちばん大事な話です。
あるノードが繰り返し実行される場合、loops= に回数が入ります。 そしてactual time も rows も、 loops で割った平均が表示されます。
Limit (cost=196523.68..196523.70 rows=10 width=30) (actual time=2160.299..2160.379 rows=10.00 loops=1)
-> Sort (cost=196523.68..196526.29 rows=1046 width=30) (actual time=2160.293..2160.320 rows=10.00 loops=1)
Sort Key: (sum((i.price * i.qty))) DESC
Sort Method: top-N heapsort Memory: 26kB
-> GroupAggregate (cost=196477.54..196501.07 rows=1046 width=30) (actual time=2090.929..2158.921 rows=20000.00 loops=1)
Group Key: c.name
-> Sort (cost=196477.54..196480.15 rows=1046 width=22) (actual time=2090.883..2131.767 rows=500000.00 loops=1)
Sort Key: c.name
Sort Method: external merge Disk: 16656kB
-> Nested Loop (cost=597.43..196425.08 rows=1046 width=22) (actual time=3.118..1561.364 rows=500000.00 loops=1)
-> Hash Join (cost=597.00..57141.20 rows=523 width=18) (actual time=3.087..177.650 rows=250000.00 loops=1)
Hash Cond: (o.customer_code = c.code)
-> Seq Scan on orders o (cost=0.00..56537.00 rows=526 width=11) (actual time=0.129..127.002 rows=250000.00 loops=1)
Filter: ((ordered_at >= '2026-07-01'::date) AND (status = 'shipped'::text) AND (channel = 'web'::text) AND (payment = 'card'::text))
Rows Removed by Filter: 1750000
-> Hash (cost=347.00..347.00 rows=20000 width=21) (actual time=2.844..2.847 rows=20000.00 loops=1)
Buckets: 32768 Batches: 1 Memory Usage: 1309kB
-> Seq Scan on customers c (cost=0.00..347.00 rows=20000 width=21) (actual time=0.053..1.144 rows=20000.00 loops=1)
-> Index Scan using order_items_order_id_idx on order_items i (cost=0.43..266.09 rows=23 width=12) (actual time=0.005..0.005 rows=2.00 loops=250000)
Index Cond: (order_id = o.id)
Filter: (qty = 3)
Rows Removed by Filter: 4
Index Searches: 250000
Planning Time: 3.419 ms
Execution Time: 2162.557 ms内側の Index Scan に loops=250000 が付いている。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF
この計画の内側はこう書いてあります。
(actual time=0.005..0.005 rows=2.00 loops=250000)
- 1 回あたり 0.005 ミリ秒
- 1 回あたり 2 行
- それが 250,000 回
総量に戻すには掛けます。0.005 × 250000 = 1250ms、2 × 250000 = 500,000 行。表示上いちばん小さい数字を持つノードが、実は 1.25 秒使っています。
PostgreSQL 18 から rows が小数で出ます(rows=2.00)。 これは平均だからです。 17 までは整数に丸められていたので、平均であることが見た目から分かりませんでした。
この変更は PostgreSQL 18 のリリースノートに載っています。同じバージョンで EXPLAIN (ANALYZE) のBUFFERS が既定で有効になり、Index Searches の行も増えました。
上の行の時間は「その下で起きたこと全部」を含む
actual time は累積です。あるノードの時間には、その子で使った時間が全部入っています。だから全ノードを足すと二重に数えることになります。
そのノード「だけ」の時間を知るには引き算します。
自分の時間 = 自分の actual time 上端 × loops
- Σ (子の actual time 上端 × 子の loops)子にも loops を掛けるのを忘れないでください。忘れると答えが変わります。実際にやってみます。
| ノード | 自分の時間 |
|---|---|
| Nested Loop | 1383.7ms(64.0%) |
| Sort | 570.4ms(26.4%) |
| Seq Scan on orders o | 127.0ms(5.9%) |
| Hash Join | 47.8ms(2.2%) |
| GroupAggregate | 27.2ms(1.3%) |
| ⋮ | |
| Index Scan using order_items_order_id_idx on order_items i(10 / 10 位) | 0.0ms(0.0%) |
| ノード | 自分の時間 | loops |
|---|---|---|
| Index Scan using order_items_order_id_idx on order_items i | 1250.0ms(57.8%) | 250000 |
| Sort | 570.4ms(26.4%) | 1 |
| Nested Loop | 133.7ms(6.2%) | 1 |
| Seq Scan on orders o | 127.0ms(5.9%) | 1 |
| Hash Join | 47.8ms(2.2%) | 1 |
左が掛け忘れた場合、右が正しい場合です。Nested Loop が 1 位から 3 位に落ち、 代わりに内側の Index Scan が 1 位に上がりました。
この引き算を全ノードでやって並べるのが、遅いノードの見つけ方の手順そのものです。
丸めの影響
actual time はミリ秒 3 桁で丸められます。0.005 と表示されていても、実際は 0.0045〜0.0055 の幅があります。
loops が大きいと、この誤差も loops 倍されます。25 万回なら ±125ms。だから loops の大きいノードの絶対値は、桁として読むのが正しい付き合い方です。
引き算した結果が負になることもあります。 そのときは親のサブツリー時間が上限を教えてくれます (親から、丸めの増幅を受けていない側の子を引いた値が、残りの子の上限)。
まとめ
EXPLAIN ANALYZEは実際に実行するactual timeは「初行まで / 全部まで」の 2 つloopsがあると表示は 1 回あたりの平均。総量は掛け算で戻す- 上の行の時間は累積。自分の時間は引き算で出す
よくある疑問
関連トピック
もっと学びたい方へ(おすすめ書籍)
「なぜこの書き方が速いのか」を実行計画から説明する一冊。条件分岐・集約・結合・更新のそれぞれで、良い書き方と悪い書き方を対比しながら読める。
テーブル設計と正規化、パフォーマンス考慮のインデックス設計まで実務レベルで学べる定番書。第2版ではクラウド対応も強化。
ER 図をどう「使える設計」に落とすか、実務の判断まで踏み込んだ入門書。エンティティの切り出しから多対多の扱いまで具体例が豊富。
ドリル 256 問を実際に打ちながら進める SQL の入門書。付属のブラウザ環境で演習できるので、SELECT から結合・集約までを環境構築で止まらずに通せる。
SQLの本質的な使い方と、インデックスが効くクエリの書き方を学べる。ウィンドウ関数など現代SQLも網羅。
実務でやりがちなSQL・DB設計のアンチパターンとその回避策を体系的に学べる。
PostgreSQLの内部構造・ストレージ・インデックス機構を丁寧に解説。設計と運用計画の鉄則が学べる。
IPAデータベーススペシャリスト試験の総合対策書。インデックス関連は本サイトと合わせて学ぶと理解が深まる。
リレーショナルモデルの理論から、インデックス設計を含む実務で使えるSQLまで解説。
本セクションはAmazonアソシエイトのリンクを含みます。
もっと深くDBを学びたい方へ。
たいてっくが、SQL・データベース設計・パフォーマンスチューニング・IPAデータベーススペシャリスト対策まで、1対1で学習をサポートします。まずは無料相談から。
「教え方も上手で、お人柄も良いメンターです。DB周りの知識はもちろん、何より、しっかり教えてあげようという姿勢がとてもありがたかったです。データベース、SQLの学習を考えている方にはおススメです。」
— H 様(DB・SQL コース受講)「体系的に知識を教えてくださり、実際の業務でも大変役立っております。特に短い時間で効率よく知識の習得や、練習をできているのは期待以上でした。」
— M 様(DB・SQL コース受講)「大変充実したコンテンツでわかりやすいご説明をありがとうございました。基本的な質問にも丁寧にご説明いただき、また業務のご相談にも乗って頂き大変有意義な時間でした。」
— K 様(DB・SQL コース受講)