This lesson on EXPLAIN for Analysts — Just Enough to Read One is hands-on and example-driven. You can inspect how Postgres executes your queries without running them by prefixing statements with EXPLAIN. You will learn how to read cost estimates, row projections, row widths, and scan types to identify full table scans, expensive sorts, and excessive data transfer across the wire.
What You'll Be Able To Do
- Interpret startup cost versus total cost numbers displayed in query execution plans
- Identify when Postgres performs a Sequential Scan, Index Scan, or Index Only Scan
- Trace execution nodes from the bottom up to locate resource-intensive operations like unindexed sorts
- Extract estimated row counts from EXPLAIN to approximate table sizes without running expensive COUNT queries
- Optimize query projection to minimize row width in bytes and reduce network transmission overhead
Detailed Concept Walkthrough
1. Anatomy of the Cost Line
The cost line in an EXPLAIN node provides Postgres's internal mathematical estimation of query work, row counts, and data volume before actual execution. It serves as a static prediction based on table statistics rather than wall-clock runtime.
- Cost Range: The two numbers separated by dots (e.g.,
cost=0.00..289.00) denote startup cost and total estimated cost in planner units. Startup cost measures the work required before the first row can be returned, while total cost estimates completing the entire node. - Rows and Width: The
rowsmetric estimates the number of rows returned by the operation based on catalog statistics. Thewidthmetric represents the average size of a single output row in bytes across all selected columns. - Best Practice: Use estimated rows from
EXPLAINinstead of runningSELECT COUNT(*)on multi-million row tables when you only require a quick approximation for dashboards or analytics.
-- Inspect cost, estimated rows, and byte width for a full table scan
EXPLAIN SELECT * FROM grades;
-- Example output line:
-- Seq Scan on grades (cost=0.00..289.00 rows=200000000 width=31)
Key Takeaway: Startup cost indicates initial pre-fetch overhead, while row width dictates the total memory and network transfer footprint.
2. Sequential Scans Versus Index Scans
Postgres selects its data access path based on available indexes, filter conditions, and the volume of rows requested. The engine chooses between reading all pages sequentially from disk heap or seeking specific index pointers.
- Sequential Scan (Seq Scan): Occurs when reading an entire table with no filtering or when no suitable index exists. The database reads disk pages directly from the table heap in sequential order, often utilizing parallel worker threads.
- Index Scan: Traverses an index structure using a specific condition (e.g.,
WHERE id = 10), finds the pointer, and fetches the corresponding table page from the heap to retrieve non-indexed columns. - Index Only Scan: Retrieves requested columns directly from the index tree without fetching data pages from the heap. This happens when every column referenced in the SELECT and WHERE clauses exists inside the index.
-- 1. Index Scan: fetches row from heap using index condition
EXPLAIN SELECT * FROM grades WHERE id = 10;
-- 2. Index Only Scan: satisfies query entirely within the index leaf pages
EXPLAIN SELECT id FROM grades WHERE id = 10;
Key Takeaway: Index Only Scans are the fastest scan path because they bypass reading table heap pages entirely.
3. Sorting and Startup Overhead
Sorting data requires distinct execution steps that inflate the startup cost, because the planner cannot emit the first row until the entire dataset is ordered. Using an indexed column avoids runtime sorting because index entries are already ordered.
- Execution Flow: Query plans must be read from the innermost/bottom node upward. Subordinate nodes fetch or scan raw records first, then pass intermediate sets to parent nodes for sorting or aggregation.
- Unindexed Sort Penalty: Sorting by an unindexed column forces Postgres to perform a sequential scan, pull all matching rows into memory or disk work areas, and sort them completely before returning row number one.
- Parallel Execution: On large unindexed sorts, the planner coordinates multiple parallel worker processes to scan and sort chunks independently, followed by a gather merge node to combine final ordered results.
-- Low startup cost: sorting on indexed column 'g' reads pre-sorted index
EXPLAIN SELECT * FROM grades ORDER BY g;
-- High startup cost: sorting unindexed column 'name' requires full table scan + sort step
EXPLAIN SELECT * FROM grades ORDER BY name;
Key Takeaway: An inflated startup cost in an execution plan typically indicates unindexed sorting, hashing, or blocking aggregation.
4. Column Selection and Row Width Optimization
The width field in an EXPLAIN plan reveals the byte payload transmitted per row, which directly impacts disk buffer throughput and client network performance.
- Data Type Sizing: Fixed-width types occupy constant bytes (e.g., standard 4-byte integers, 8-byte doubles), whereas variable types like
TEXTcontribute average estimated byte lengths based on table statistics. - Network Footprint: Selecting unnecessary columns or blanket
SELECT *inflates row width, multiplying memory consumption during sorts and generating higher TCP packet overhead across application connections. - Best Practice: Explicitly project only required columns in production analytics queries to keep row width small and unlock index-only execution paths.
-- Compare width: single 4-byte integer column
EXPLAIN SELECT id FROM grades;
-- -> (width=4)
-- Compare width: text column with variable average length
EXPLAIN SELECT name FROM grades;
-- -> (width=19)
Key Takeaway: Pruning unneeded columns reduces row byte width, memory usage during intermediate operations, and network transfer volume.
Topics Covered in EXPLAIN for Analysts — Just Enough to Read One
- Introduction to EXPLAIN (0:00 - 0:45) — Sets up the 200-million row sample table and explains the purpose of the EXPLAIN command.
- Decoding the Cost Line (0:45 - 1:55) — Breaks down startup cost, total cost units, estimated row counts, and row width in bytes.
- Indexed ORDER BY Sorting (1:55 - 2:30) — Demonstrates low startup cost sorting when using a pre-ordered indexed column.
- Unindexed Sorting and Parallelism (2:30 - 3:25) — Examines parallel sequential scans and huge startup cost spikes during unindexed sorts.
- Row Width and Column Selection (3:25 - 4:20) — Shows how narrowing projected columns reduces row width from 31 bytes down to 4 bytes.
- Index Scan vs Index Only Scan (4:20 - 5:10) — Compares heap lookups in standard Index Scans against zero-heap Index Only Scans.
SQL Cheat Sheet
-
EXPLAIN <query>— Shows estimated planner cost and execution node hierarchy without executing queryEXPLAIN SELECT * FROM grades; -
cost=startup..total— Represents units to first row and units to completion-- Seq Scan on grades (cost=0.00..289.00 rows=200000000 width=31) -
rows=<count>— Estimates total rows emitted by that execution plan node-- rows=200000000 -
width=<bytes>— Displays average byte size of single output row-- width=4 -
Seq Scan— Scans raw table heap pages sequentially without using indexesEXPLAIN SELECT * FROM grades; -
Index Scan— Traverses index to find row pointers, then reads heapEXPLAIN SELECT * FROM grades WHERE id = 10; -
Index Only Scan— Returns data directly from index without accessing table heapEXPLAIN SELECT id FROM grades WHERE id = 10;
Comparison Table
| Scan Method | Heap Access Required | Typical Use Case |
|---|---|---|
| Seq Scan | Full heap scan | No filters or broad full-table reads |
| Index Scan | Selective heap fetch | Filtered queries retrieving unindexed columns |
| Index Only Scan | Zero heap fetch | Filtered queries selecting only indexed columns |
Common Pitfalls
- Mistake: Assuming EXPLAIN cost represents exact execution time in milliseconds. Avoid: Treat EXPLAIN cost as planner work units and use EXPLAIN ANALYZE for actual runtimes.
- Mistake: Running SELECT COUNT(*) across large tables for rough counts. Avoid: Read the estimated rows metric from EXPLAIN to avoid expensive table-wide scans.
- Mistake: Using SELECT * when sorting or filtering large datasets. Avoid: Project only necessary columns to decrease byte width and avoid unnecessary heap fetches.
- Mistake: Reading nested EXPLAIN plans from top to bottom. Avoid: Read execution plans from the bottom up to follow actual data flow.
FAQs
- Does EXPLAIN execute my SQL query behind the scenes? No, EXPLAIN only generates the planner's estimated execution strategy without executing the query or modifying data.
- What does a startup cost of 0.00 mean? It means the database engine can immediately begin streaming rows from disk heap without completing preparatory work like sorting or grouping.
- Why is an Index Only Scan faster than a standard Index Scan? An Index Only Scan satisfies the query entirely from index memory pages, eliminating the secondary step of fetching table heap pages.