PostgreSQLX · Query Planning, Indexes and Performance MethodPlanner
How the planner chooses
What you'll learn
- Reproduce a sequential scan cost from the catalogue and the cost constants
- Name the scan and join types and say when each is appropriate
- Explain what random_page_cost and effective_cache_size actually change
- Decide whether a cost setting is worth adjusting on your hardware
Prerequisites
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
The planner enumerates ways to execute a query, assigns each a cost, and runs the cheapest. The cost is in arbitrary units anchored to one sequential page read, and it is arithmetic rather than judgement — which means you can check it.
Reproducing a cost
$ psql -U postgres -c "EXPLAIN SELECT * FROM ordersx" Seq Scan on ordersx (cost=0.00..36667.00 rows=2000000 width=32)$ psql -U postgres -c "SELECT relpages, reltuples::bigint, relpages*current_setting('seq_page_cost')::numeric + reltuples*current_setting('cpu_tuple_cost')::numeric AS predicted FROM pg_class WHERE relname='ordersx'" relpages | reltuples | predicted
----------+-----------+-----------
16667 | 2000000 | 3666716667 × 1 + 2000000 × 0.01 = 36667. Exactly the number in the plan.
The two numbers in cost=0.00..36667.00 are startup cost and total
cost. Startup is what must happen before the first row can be
returned — zero for a sequential scan, substantial for a sort, which is
why LIMIT changes plan choices so dramatically.
The cost constants
| Constant | Default | Represents |
|---|---|---|
seq_page_cost | 1.0 | Reading one page sequentially. The unit. |
random_page_cost | 4.0 | Reading one page at a random offset |
cpu_tuple_cost | 0.01 | Processing one row |
cpu_index_tuple_cost | 0.005 | Processing one index entry |
cpu_operator_cost | 0.0025 | Evaluating one operator or function |
effective_cache_size | 4 GB | How much cache the planner assumes exists |
Scan types
| Type | Reads | Best when |
|---|---|---|
| Seq Scan | Every page in order | A large fraction of the table is needed |
| Index Scan | Index, then heap rows one at a time in index order | Few rows, or the order is wanted |
| Index Only Scan | Index alone, if the visibility map allows | Every needed column is in the index |
| Bitmap Heap Scan | Index builds a page bitmap, then heap in page order | A moderate fraction; several indexes combined |
The bitmap variant exists for the middle ground. Rows in index order are scattered across the heap, so a plain index scan revisits pages; the bitmap sorts them so each page is read once.
$ psql -U postgres -c "EXPLAIN (ANALYZE, BUFFERS, COSTS ON, TIMING OFF, SUMMARY OFF) SELECT sum(amount) FROM ordersx WHERE customer_id BETWEEN 1 AND 3000" -> Bitmap Heap Scan on ordersx (cost=422.75..18066.13 rows=28324 width=4) (actual rows=30000.00 loops=1)
Recheck Cond: ((customer_id >= 1) AND (customer_id <= 3000))
Heap Blocks: exact=260
Buffers: shared hit=241 read=56
-> Bitmap Index Scan on ordersx_customer_idx (cost=0.00..415.67 rows=28324 width=0) (actual rows=30000.00 loops=1)
Buffers: shared hit=37Where the crossover actually is
$ psql -U postgres -c "EXPLAIN (ANALYZE, BUFFERS, COSTS ON, TIMING OFF, SUMMARY OFF) SELECT count(*) FROM ordersx WHERE customer_id BETWEEN 1 AND N" rows matched | plan chosen | buffers
--------------+--------------------------+---------
2 000 | Index Only Scan | 6
200 000 | Parallel Index Only Scan | 236
2 000 000 | Parallel Seq Scan | 16667The switch happened between 10% and 100% selectivity here. The “indexes
stop helping past 5%” rule that circulates is not a constant: it depends
on random_page_cost, effective_cache_size, index correlation and row
width. Measure it on your data rather than assuming it.
Join types
| Type | Mechanism | Chosen when |
|---|---|---|
| Nested Loop | For each outer row, look up matching inner rows | The outer side is small |
| Hash Join | Build a hash of one side, probe with the other | Both sides substantial, join on equality |
| Merge Join | Sort both sides, walk them together | Both already sorted, or sorting is cheap |
The nested loop is where bad estimates become disasters, because its cost is multiplied by the outer row count. A plan expecting 12 outer rows and receiving 1,000 does 83 times the work it was costed for. Lesson X-04 measures exactly that.
What to take from this
- Cost is arithmetic.
relpages × seq_page_cost + reltuples × cpu_tuple_costreproduced a plan’s number exactly. random_page_cost = 4encodes a spinning disk. On flash it should be closer to 1.1.effective_cache_sizeallocates nothing and changes index costing. The 4 GB default is low for modern servers.enable_*settings are diagnostics, not fixes. On 18.6 they mark a node type as disabled —Disabled: truein the plan — rather than inflating its cost.- The index-to-sequential crossover is not a fixed percentage. Measure it.
Workers PlannedagainstWorkers Launchedis where intermittent parallel slowness shows up.
Cross-course references
- Linux for Production Sysadmins — Part XLI (Storage Performance)
covers measuring the random-versus-sequential ratio that
random_page_costis supposed to express, so the setting comes from a device rather than from a blog post. - Observability for Production Sysadmins — Part LIX (Database observability) covers tracking plan-shape changes rather than discovering them during an incident.
Quiz
Knowledge check · 6 questions
Q1. A cluster runs on local NVMe and its planner keeps choosing sequential scans where an index would clearly be faster. Which setting is the first candidate?
Q2. An engineer adds SET enable_seqscan = off to an application's session setup because it fixed one slow query. What is the risk?
Q3. A plan shows 'Workers Planned: 4' but the execution reports 'Workers Launched: 0'. What does this indicate?
Q4. Which statements about effective_cache_size are correct? Select all that apply.
Q5. The cost of a sequential scan can be reproduced exactly as relpages times seq_page_cost plus reltuples times cpu_tuple_cost.
Q6. Why does the widely repeated rule that indexes stop helping past about 5% selectivity not hold as a constant?
Passing score: 75%. Answers are checked in this browser.