> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Reading EXPLAIN ANALYZE Like a Map Rather Than a Wall
- URL: https://debugly.dev/reading-explain-analyze-like-map/
- Published: 2026-10-09T15:16:00.000Z
- Updated: 2026-10-17T05:56:07.000Z
- Description: The plan output looked like noise until the nested loops were read as a route and the row estimate gap pointed straight at the missing statistic, and the…
- Author: Rohit Bhadani
- Tags: Tooling, PostgreSQL, Performance

Start at the nine seconds and you miss it. Start at the forty milliseconds and you miss how it arrived. The bug lived in the hand-off.

This is the anatomy of reading a query plan, and of the specific property that makes it readable: the plan is a tree, and the tree is a route, and the route has one expensive step, and the expensive step is the diagnosis, and the diagnosis is the number that disagrees with itself.

This was Postgres 16, and the mechanics are the same for any database that exposes its plan.

## Why the plan looks like a wall

The plan is the text, and the text is the indented lines, and the lines are the nodes, and the nodes are the operations, and the operations are the scan and the join and the sort, and the three are the vocabulary, and the vocabulary is the reading, and the reading is the tree, and the tree is the indentation, and the indentation is the nesting, and the nesting is the route, and the route is the map, and the map is the fix's entry point.

The wall is the unread, and the unread leads to the guess, and the guess is the index that does not help, and the still-slow query is what finally forces the reading. The reading is the discipline, and the discipline is four numbers per node: the estimate, the actual, the rows, and the time, and the four are the map's legend.

The plan is not noise, it is the database explaining itself, and the explanation is the diagnosis handed to you for free.

## The four numbers that matter

**The estimated rows against the actual rows.** The estimate is the planner's guess, and the actual is the truth, and the truth against the guess is the gap, and the gap is the diagnosis, because a large gap means the planner did not know the data, and the not knowing is the statistic, and the statistic is the fix, and the fix is the analyze, and the analyze is the one command.

**The actual time on each node.** The time is the cost, and the cost is the distribution, and the distribution is the expensive node, and the expensive node is the target, and the target is the fix, and the fix is the node's operation, and the operation is the scan or the join, and the scan is the index, and the join is the order, and the two are the fixes.

**The loops count.** The loops are the repetitions, and the repetitions are the multiplication, and the multiplication is the nested loop's cost, and the cost is the inner side times the loops, and the times is the total, and the total is the nine seconds, and the nine seconds is the fix's target, and the target is the loops' reduction.

**The rows removed by filter.** The removed are the wasted work, and the wasted is the scan's inefficiency, and the inefficiency is the index's absence, and the absence is the fix, and the fix is the index, and the index is the filter's pushdown, and the pushdown is the reduction.

## How the gap gave the answer

The plan showed a nested loop, and the loop's inner side was a scan, and the scan's estimate was ten rows, and the actual was four hundred thousand, and the four hundred thousand against the ten was the gap, and the gap was the planner's ignorance, and the ignorance was the statistic, and the statistic was the stale, and the stale was the table's growth, and the growth was the last analyze, and the analyze was the fix, and the fix was the one command, and the command was the forty milliseconds.

The planner chose the nested loop because it believed ten rows, and the ten rows made the loop cheap, and the cheap was the choice, and the choice was the four hundred thousand, and the four hundred thousand was the nine seconds, and the nine seconds was the estimate's fault, and the fault was the statistic, and the statistic was the fix, and the fix was the analyze, and the analyze updated the estimate, and the updated estimate changed the plan, and the changed plan was the hash join, and the hash join was the forty milliseconds.

This is the same discipline as [the pg\_stat\_statements ledger that showed the slowest queries](https://debugly.dev/pg-stat-statements-ledger/), and the shared property is the one worth fixing.

## The method

**Run EXPLAIN with ANALYZE and BUFFERS.** The analyze gives the actual times, and the buffers give the IO, and the two are the truth, and the truth is the reading, and the reading is the fix, and the fix starts with the real numbers rather than the plan alone.

**Read the tree from the deepest indented node up.** The deepest is the first executed, and the first is the innermost, and the innermost is the loops' body, and the body is the multiplication, and the multiplication is the cost, and the cost is the target.

**Find the largest estimate-to-actual gap.** The gap is the planner's error, and the error is the wrong plan, and the wrong plan is the slow query, and the slow is the fix, and the fix is the statistic, and the statistic is the analyze.

**Find the node with the largest actual time.** The time is the cost, and the cost is the target, and the target is the operation, and the operation is the fix's subject, and the subject is the index or the join or the sort.

## What I now do

A query plan is a tree of operations with four numbers per node, and the diagnosis is the largest gap between the estimated and the actual row counts, plus the node with the largest actual time. Run EXPLAIN ANALYZE BUFFERS, read from the deepest node up, and fix the estimate gap before adding indexes.

The query took nine seconds and the plan was forty lines of text that looked like noise, and the noise contained one number that disagreed with another by a factor of forty thousand. I now check for that property first, because it is where the failure actually lives.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)