Course 5 of 6 · f1db & chinook datasets

Read Query Plans

Fluent EXPLAIN reading — every node type, its performance profile, and what to do about it.

8Modules
3Core
4Advanced
1Architect
Module 01

Introduction to EXPLAIN

Core · 5 topics

The EXPLAIN output reports five numbers per node: startup cost, total cost, estimated rows, row width, and — with ANALYZE — actual timing. This module teaches you to read those numbers and locate the bottleneck node.

Book reference: Chapter 9 — Indexing Strategy

01
Basic EXPLAIN syntaxRun EXPLAIN on a query and read the plan output it produces.
02
Plan tree structureNavigate the plan tree structure by reading node indentation.
03
Cost anatomy: startup vs total costDistinguish startup cost from total cost and explain the difference.
04
Sequential Scan node and the Filter annotationSpot filter annotations that signal missing index opportunities.
05
EXPLAIN options: VERBOSE, SETTINGS, WAL, GENERIC_PLANUse EXPLAIN options to expose extra plan detail when needed.

Free preview: Reading EXPLAIN

EXPLAIN output anatomy
Identify and locate each cost number in an EXPLAIN line.
Module 01 · Startup vs total cost
-- startup cost = work before the first row
-- total cost   = work to return all rows
-- High startup signals sorting or hashing.
explain
select d.surname,
       sum(res.points) as career_points
  from f1db.drivers  d
  join f1db.results res
    on res.driverid = d.driverid
 group by d.driverid, d.surname
 order by career_points desc
 limit 10;

EXPLAIN plan for the top 10 drivers ranked by total career points across all seasons.

Module 02

EXPLAIN ANALYZE in Practice

Core · 5 topics

EXPLAIN ANALYZE runs the query and adds actual rows, actual time, and loop counts. The gap between estimated and actual rows is the key signal — it tells you where the planner's model breaks down.

Book reference: Chapter 9 — Indexing Strategy

01
Actual timing and row counts (actual time, actual rows, loops)Add actual timing and row counts to any EXPLAIN output.
02
Running EXPLAIN ANALYZE safelySafely measure write queries without committing unwanted changes.
03
Actual vs estimated row divergenceIdentify where the planner's row estimates diverge from reality.
04
LoopsCorrectly interpret row counts and timing in looping plan nodes.
05
BUFFERS: shared hit vs read; track_io_timingDistinguish cache hits from disk reads in BUFFERS output.

Free preview: Reading EXPLAIN

EXPLAIN ANALYZE actual vs estimated rows
Spot the gap between estimated and actual row counts.
Module 02 · Estimated vs actual rows
-- rows=N (estimate) vs actual rows=N
-- A large gap means stale statistics or
-- correlated predicates.
explain (analyze, buffers)
select r.year,
       r.round,
       r.name
  from f1db.races r
 where r.year  = 2017
   and r.round <= 10;

EXPLAIN ANALYZE output for 2017 season races filtered to the first ten rounds.

Module 03

Plan Node Types: Scans and Joins

Core · 2 topics

The executor processes a tree of plan nodes, each producing tuples for its parent until the root node drives the whole thing. This module covers the scan and join nodes that fetch and combine rows.

Book reference: Chapter 9 — Indexing Strategy

01
Scan nodes: Seq Scan, Index Scan, Index Only Scan, Bitmap Index/Heap Scan, BitmapAnd/BitmapOr, Function Scan, GiST kNN ScanRecognize each scan node type and when the planner picks it.
02
Join nodes: Hash Join, Nested Loop (with Memoize), Merge Join, Semi Joins, Anti JoinsIdentify which join algorithm appears and why the planner chose it over the others.

Free preview: Reading EXPLAIN

Scan and join nodes at a glance
Match each scan and join node to when the planner picks it.
Module 03 · Hash Join node
-- Hash Join: build a hash table from the
-- smaller relation, probe with each row
-- from the larger. Build cost = startup.
explain (analyze, buffers)
select d.nationality,
       count(distinct d.driverid)   as drivers,
       count(*) filter(
         where res.positionorder=1) as wins
  from f1db.results res
  join f1db.drivers d
    on d.driverid = res.driverid
  join f1db.races   r
    on r.raceid   = res.raceid
 where r.year = 2017
 group by d.nationality
 order by wins desc;

EXPLAIN ANALYZE for driver and win counts by nationality in the 2017 season.

Module 04 · Advanced

Plan Node Types: Aggregation and Parallel Execution

Advanced · 3 topics

After scans and joins fetch and combine rows, this module covers what happens next: grouping, ordering, combining results from sub-plans, and running parts of the plan across multiple workers.

Book reference: Chapter 9 — Indexing Strategy

01
Aggregation and sort nodes: Sort, Incremental Sort, HashAggregate, GroupAggregate, WindowAggDistinguish Sort from Incremental Sort and pick the aggregate node that matches your query.
02
Set operations, subplans, and utility nodes: Append/MergeAppend, partition pruning, Recursive Union, InitPlan/SubPlan, CTE Scan, Limit, MaterializeRead plans for UNION ALL, recursive CTEs, correlated subqueries, and partitioned tables.
03
Parallel nodes: Gather, Gather Merge, Partial/Finalize Aggregate, and JIT compilation markersUnderstand parallel plan structure and locate the worker sub-plans it coordinates.

Free preview: Reading EXPLAIN

Aggregation and parallel plan nodes
Locate Gather nodes and the worker sub-plans they coordinate.
Module 04 · WindowAgg plan node
-- Every window query needs a Sort node
-- to feed the WindowAgg node.
-- Incremental Sort can reduce this cost.
explain (analyze, buffers)
select r.year,
       d.surname,
       sum(res.points) over(
         partition by r.year, d.driverid
         order by r.date
         rows between unbounded preceding
                  and current row
       ) as running_points
  from f1db.results res
  join f1db.races   r
    on r.raceid   = res.raceid
  join f1db.drivers d
    on d.driverid = res.driverid
 where r.year = 2017
   and d.driverid in (1, 8, 20)
 order by r.date, d.surname;

Running cumulative points per driver by race date for three drivers in the 2017 season.

Module 05 · Advanced

Statistics and the Planner

Advanced · 3 topics

Every `rows=` figure in a plan is an estimate computed from statistics ANALYZE collected — not a measurement. This module traces row estimates back to the MCV list and histogram that produced them.

Book reference: Chapter 9 — Indexing Strategy

01
What ANALYZE collects, and reading an estimate from the MCV list or the histogramTrace a row estimate back to the exact statistics row it came from.
02
What default_statistics_target buys youDecide when a larger statistics sample is worth the extra ANALYZE cost.
03
Correlation and cost, not just row count; estimates are assumptions, not measurementsRecognize when the planner's independence assumption is producing a bad estimate.

Free preview: Reading EXPLAIN

Statistics and row estimates
See how estimate errors compound through the plan tree.
Module 05 · Correlated column misestimate
-- geoname.class and .feature are strongly
-- correlated: 'PPL' always implies class 'P'.
-- The planner multiplies selectivities as
-- if they were independent and lands far
-- below the actual row count.
explain (analyze, buffers)
select name, population
  from geoname.geoname
 where class   = 'P'
   and feature = 'PPL';

EXPLAIN ANALYZE plan for geoname rows matching both class 'P' and feature 'PPL'.

Module 06

Detecting Performance Issues

Advanced · 4 topics

Batches > 1 on a HashJoin means the hash table spilled to disk. Sort Method: external merge means the sort did too. Rows × loops mismatches point to bad estimates compounding through the tree.

Book reference: Chapter 9 — Indexing Strategy

01
Row estimate mismatchTrace how a single bad estimate degrades the entire plan.
02
Expensive SortRecognize when a Sort node is the plan's bottleneck.
03
Hash Join batchesDetect when a Hash Join spills to disk and why.
04
Rows Removed by FilterUse filter row counts to identify missing index candidates.

Free preview: Reading EXPLAIN

Plan tree structure
Follow execution order from leaf nodes up to the root.
Module 06 · Row estimate mismatch
-- Two correlated predicates: positionorder=1
-- and points > 20 are not independent.
-- The planner multiplies their selectivities
-- and underestimates the actual row count.
explain (analyze, buffers)
select res.raceid,
       res.driverid,
       res.positionorder,
       res.points
  from f1db.results res
 where res.positionorder = 1
   and res.points > 20;

Results rows where finishing position is first and points scored exceed 20.

Module 07 · Advanced

Building a Diagnostic Workflow

Advanced · 4 topics

pg_stat_activity for live query monitoring, lock monitoring with pg_locks, and CREATE STATISTICS for correlated columns. How to triage a slow-query incident without disturbing production traffic.

Book reference: Chapter 9 — Indexing Strategy

01
Diagnostic step 1: scan ratios from pg_stat_user_tablesQuery pg_stat_user_tables to find tables lacking effective indexes.
02
Diagnostic step 2: plan comparisonCompare plans before and after a change to confirm improvement.
03
Machine-readable plans with FORMAT JSON (explain.depesz.com, pev2)Export plans in JSON format for use with visualization tools.
04
Repeatable benchmarkingRun repeatable benchmarks that account for cache warming effects.

Free preview: Reading EXPLAIN

Systematic diagnostic workflow
Connect plan signals to system-level root causes systematically.
Module 07 · Baseline plan comparison
-- Step 1: capture the default plan and cost.
-- Step 2: disable one node type to force an
-- alternative, then compare actual time.
explain (analyze, buffers)
select d.surname,
       sum(res.points) as points
  from f1db.results res
  join f1db.drivers d
    on d.driverid = res.driverid
 group by d.driverid, d.surname
 order by points desc
 limit 10;

-- Then compare:
-- set enable_hashjoin = off;
-- ...same query...
-- reset enable_hashjoin;

Total career points per driver ranked descending, limited to the top 10.

Module 08 · Architect

Observability and Performance Strategy

Architect · 6 topics

VACUUM signals in EXPLAIN output, autovacuum tuning, and pg_stat_user_tables for table-level health. Connecting plan-level observation to database-wide performance management.

Book reference: Chapter 9 — Indexing Strategy

01
Currently running queries: pg_stat_activity (state, wait_event, query)Monitor live queries and identify wait events using system views.
02
Table bloat and VACUUMDetect table bloat and understand its effect on plan performance.
03
Lock monitoringIdentify lock contention that slows otherwise efficient queries.
04
Statistics management: pg_stats, default_statistics_target, CREATE STATISTICSManage column statistics to improve planner row estimates.
05
Planner limitations: join search space, independence assumption, generic plansRecognize planner limitations that cause systematic misestimates.
06
Chinook: confident inner-join cardinality with enforced foreign keysUnderstand how schema constraints affect the planner's join decisions.

Free preview: Reading EXPLAIN

I/O patterns and table bloat
Link plan-level degradation to physical table health metrics.
Module 08 · chinook FK-enforced plan
-- chinook declares foreign keys; f1db does not.
-- Declared FKs give the planner join-elimination
-- opportunities it cannot use without them.
explain (analyze, buffers)
select ar.name                    as artist,
       count(distinct al.album_id) as albums,
       count(t.track_id)           as tracks,
       round(
         sum(t.milliseconds)
           / 60000.0::numeric, 1
       )                           as total_minutes
  from chinook.artist ar
  join chinook.album  al
    on al.artist_id = ar.artist_id
  join chinook.track  t
    on t.album_id   = al.album_id
 group by ar.artist_id, ar.name
 order by total_minutes desc
 limit 15;

Album count, track count, and total playing time per chinook artist, top 15 by runtime.

All 6 courses. One package.

Core, Advanced, and Architect tiers — 48 modules across 6 PostgreSQL courses.

Get All 6 Courses Browse All 6 Courses