Skip to main content
RunBook Academy

PostgreSQLX · Query Planning, Indexes and Performance MethodPlanner

How the planner chooses

Intermediate⏱ ~30 minpsql

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

Not yet marked complete on this device.

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

Read-only / Safethe planner's number
$ psql -U postgres -c "EXPLAIN SELECT * FROM ordersx"
 Seq Scan on ordersx  (cost=0.00..36667.00 rows=2000000 width=32)
Read-only / Safethe same number, computed from the catalogue
$ 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 |     36667

16667 × 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

ConstantDefaultRepresents
seq_page_cost1.0Reading one page sequentially. The unit.
random_page_cost4.0Reading one page at a random offset
cpu_tuple_cost0.01Processing one row
cpu_index_tuple_cost0.005Processing one index entry
cpu_operator_cost0.0025Evaluating one operator or function
effective_cache_size4 GBHow much cache the planner assumes exists

Scan types

TypeReadsBest when
Seq ScanEvery page in orderA large fraction of the table is needed
Index ScanIndex, then heap rows one at a time in index orderFew rows, or the order is wanted
Index Only ScanIndex alone, if the visibility map allowsEvery needed column is in the index
Bitmap Heap ScanIndex builds a page bitmap, then heap in page orderA 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.

Read-only / Safea bitmap heap scan reading 30,000 rows from 260 pages
$ 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=37

Where the crossover actually is

Read-only / Safethe same query shape, widening the range
$ 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        |   16667

The 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

TypeMechanismChosen when
Nested LoopFor each outer row, look up matching inner rowsThe outer side is small
Hash JoinBuild a hash of one side, probe with the otherBoth sides substantial, join on equality
Merge JoinSort both sides, walk them togetherBoth 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_cost reproduced a plan’s number exactly.
  • random_page_cost = 4 encodes a spinning disk. On flash it should be closer to 1.1.
  • effective_cache_size allocates 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: true in the plan — rather than inflating its cost.
  • The index-to-sequential crossover is not a fixed percentage. Measure it.
  • Workers Planned against Workers Launched is 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_cost is 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

  1. 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?

  2. Q2. An engineer adds SET enable_seqscan = off to an application's session setup because it fixed one slow query. What is the risk?

  3. Q3. A plan shows 'Workers Planned: 4' but the execution reports 'Workers Launched: 0'. What does this indicate?

  4. Q4. Which statements about effective_cache_size are correct? Select all that apply.

  5. Q5. The cost of a sequential scan can be reproduced exactly as relpages times seq_page_cost plus reltuples times cpu_tuple_cost.

  6. 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.