What's changed: Initial version (topic G2)
2.3Query Execution Plans
Learn to read execution plans: EXPLAIN (visualizes the plan) versus EXPLAIN ANALYZE (actually executes and reports real measurements), join strategies Nested Loop, Hash Join, and Merge Join, scan types Seq Scan, Index Scan, and Bitmap Scan, window functions, and parallel query (max_worker_processes, max_parallel_workers_per_gather) that splits work across multiple processes.
The statistics from the previous sections were the material the planner uses to build an execution plan. This section is about reading the resulting plan itself. EXPLAIN sits at the core of the Gold exam—accurately reading which join strategy, which scan type, and in what order a query executes is directly tied to diagnosing performance problems.
2.3.1Reading EXPLAIN and EXPLAIN ANALYZE
- EXPLAIN shows the plan the planner chose, as a tree, without actually running the query. Each node carries
cost=startup cost..total cost(the planner's estimate, in abstract cost units),rows=estimated row count, andwidth=average row width—estimates only, with no actual measurements. - EXPLAIN ANALYZE actually runs the query and additionally reports each node's
actual time=start..end(in milliseconds),actual rows=measured row count, andloops=how many times this node executed. Its greatest value is that it lets you compare estimate against reality—a large gap betweenrows(estimated) andactual rows(measured) hints that statistics are stale or inaccurate. Running it against DML with side effects (INSERT/UPDATE/DELETE) actually changes data, so wrap it in a transaction and ROLLBACK, or restrict its use to read-only queries, when you only want to inspect the plan. - The plan tree executes from the innermost (most deeply indented) node outward, with results flowing up to enclosing nodes. Leaf nodes like
Seq Scanfetch actual data, intermediate nodes likeHash Joincombine them, and top-level nodes likeLimitorSortdo final shaping—the basic reading order is bottom to top.
2.3.2Join strategies, scan types, window functions, and parallel query
- Join strategies: Nested Loop (scans the inner side once per outer row; favorable for small tables, or when the inner side has an effective index); Hash Join (builds an in-memory hash table from one side—usually the smaller—and scans the other once to match; favorable for equi-joins with large volumes); Merge Join (both sides are sorted by the join key, then merged in one pass; chosen when both are already sorted, or when paying the sort cost is still worthwhile).
- Scan types: Seq Scan (reads the whole table sequentially from the start; the default when many rows match or no index helps); Index Scan (looks up matching rows via an index, accessing the heap for each one; favorable when few rows match); Bitmap Scan (
Bitmap Index Scanbuilds a bitmap of matching blocks from the index, thenBitmap Heap Scanreads just those blocks in one pass; chosen when the matching row count is intermediate—too many for Index Scan to stay efficient, but not enough to justify Seq Scan—reducing random I/O by batching heap access at the block level). - Window functions use
OVER (PARTITION BY ... ORDER BY ...)to attach a computed value to each row without collapsing rows via aggregation (ROW_NUMBER(),RANK(), running sums, etc.). In the plan they appear as a dedicatedWindowAggnode, which typically requires the rows to be sorted by thePARTITION BY/ORDER BYcolumns beforehand. - Parallel query is the mechanism for splitting large Seq Scans or aggregations across multiple background workers. It is governed by max_worker_processes (the total ceiling on background workers usable server-wide) and max_parallel_workers_per_gather (the ceiling on parallel workers a single
Gathernode can use), and appears in the plan as aGathernode (which collects each worker's results) alongsideParallel Seq Scan.
The staples: EXPLAIN is estimate-only; EXPLAIN ANALYZE actually executes and reports real measurements too (beware DML side effects); Nested Loop suits small data / an effective inner index, Hash Join suits equi-joins with large volumes, Merge Join assumes already-sorted input; Bitmap Scan is the middle ground between Seq and Index Scan for an intermediate row count; a gap between rows and actual rows suggests stale statistics; the Gather node is where parallel query results converge. Understanding that join strategy and scan type are two independent axes (how to combine data vs. how to fetch it) is tested.
Let us practice reading real EXPLAIN ANALYZE output. Suppose a typical query joins orders (millions of rows) to customers (tens of thousands) on customer_id, filtered by WHERE customers.region = 'tokyo'. If the top-level line reads Hash Join (cost=1250.00..48000.00 rows=15000 width=64) (actual time=12.500..350.200 rows=14800 loops=1), the planner chose a Hash Join, and the estimated 15000 rows nearly matches the measured 14800—statistics look healthy. If the child nodes below it are Hash (cost=800.00..800.00 rows=5000 width=32) (actual time=8.000..8.000 rows=4820 loops=1) → Seq Scan on customers (cost=0.00..800.00 rows=5000 width=32) (actual time=0.010..6.500 rows=4820 loops=1) Filter: (region = 'tokyo'::text), you can read that the smaller customers side is filtered first, then hashed (the Hash node), while the other child, Seq Scan on orders (cost=0.00..42000.00 rows=3000000 width=40) (actual time=0.020..180.000 rows=3000000 loops=1), scans all of orders to match against it. If instead rows=15000 (estimated) diverges from actual rows=140000 (measured) by an order of magnitude or more, suspect that statistics on region are stale, or that there is an unregistered correlation between customers.region and some column on the orders side—consider re-running ANALYZE customers; or adding extended statistics. Also, for the same query, if an index idx_customers_region exists on region and the matching rows are a tiny fraction of the table (say, under 1%), the planner is more likely to choose Index Scan using idx_customers_region on customers instead of Seq Scan on customers—or, for an intermediate row count, Bitmap Heap Scan on customers plus Bitmap Index Scan on idx_customers_region. The key point in reading a plan is that the scan strategy changes with the estimated row count, even for the same table and the same condition. For analytical queries involving aggregation (e.g., computing cumulative sales by region), a WindowAgg node appears, and you check whether the plan includes a preceding Sort (on the PARTITION BY/ORDER BY columns). When aggregating across the entire large orders table, seeing a Parallel Seq Scan on orders beneath a Gather node means parallel query kicked in and work was split across multiple workers, with max_parallel_workers_per_gather acting as the ceiling on how many workers that Gather node actually used.
| Type/strategy | Typically chosen when | Plan notation |
|---|---|---|
| Nested Loop | Small data, effective index on inner side | Nested Loop |
| Hash Join | Equi-join, large volume | Hash Join + Hash |
| Merge Join | Both sides sorted / sort acceptable | Merge Join + Sort |
| Seq Scan | Many matching rows / no usable index | Seq Scan on <table> |
| Index Scan | Few matching rows | Index Scan using <idx> |
| Bitmap Scan | Intermediate matching row count | Bitmap Heap Scan + Bitmap Index Scan |
Trap: "EXPLAIN alone tells you the query's actual duration" is wrong—EXPLAIN is estimate-only and includes no real timing; use EXPLAIN ANALYZE when you need actual measurements. Also, "EXPLAIN ANALYZE is safe to use on DML because it doesn't execute the query" is wrong—EXPLAIN ANALYZE actually executes the query, so running it against DML (INSERT/UPDATE/DELETE) really changes data. If you only want to inspect the plan, wrap it in a transaction and ROLLBACK, or take similar precautions.
2.3.3Section summary
- EXPLAIN = estimates only; EXPLAIN ANALYZE = executes and also reports actual measurements (mind DML side effects). Read plans from the bottom (inner) upward
- Join strategy (Nested Loop/Hash Join/Merge Join) and scan type (Seq/Index/Bitmap) are independent axes. WindowAgg = window functions; Gather = where parallel query results converge (controlled by max_worker_processes/max_parallel_workers_per_gather)
Sign in to track progress — Log in.
Quick check
(just a quick review)Q1. You want to see not just the plan's estimates but also the actual time taken and actual row counts when it runs. Which command should you use?
Q2. A query performs an equi-join between a table of tens of thousands of rows and one with millions of rows, and the plan includes a node that builds an in-memory hash table from the smaller table. Which join strategy does this describe?
Q3. The plan for an aggregation query over the entire large orders table shows a Gather node with Parallel Seq Scan on orders beneath it. Which statement about this plan is correct?

