Skip to content
In this guide

Guide · The plan, node by node

Loops

loops=N is the last figure on every node of an EXPLAIN ANALYZE plan. It says how many times the node ran. The rows and the time printed beside it are averages per run, not totals. The sample's Index Scan using order_items_order_idx on order_items prints rows=3 loops=206880: 3 rows each time, 206,880 times, 620,640 rows in all. Index Scan using customers_pkey on customers prints rows=1 loops=620640.

Why it matters

Read the rows without the loops and the plan lies to you. An index scan that returns 3 rows sounds free. 206,880 of them cost more than the Seq Scan they hang off. The node under a Nested Loop runs once per outer row, so its loop count is the outer side's row count. When the outer estimate is 338× off, the loop count is 338× off with it. Under a Gather, loops is the number of processes.

What to do

  1. Multiply before judging: rows × loops, time × loops.
  2. If the loop count is far above the planner's estimate for the outer side, fix that estimate first. ANALYZE commerce.orders takes the sample from 206,880 loops to one Hash Join pass.
  3. If the loop count is right, make each loop cheaper: an index that answers the lookup from one page, or an Index Only Scan.

In the Plan tab

Seqora multiplies for you. Every band is rows × loops, and the loop count sits under the band as ↻ ×N. Marker 4 is the one this page is about.

The row flow of the sample, with the loop label under the order_items scan.

The row flow of the sample, with the loop label under the order_items scan.

  1. The band into Seq Scan on orders is the 12,000,000 rows read in its one run. A scan that runs once has no loop label.
  2. 207K ┅ 612 is the scan's output, 206,880 rows, against the planner's 612. Every node to the right of it under the Nested Loop runs once per one of these rows.
  3. −11.8M is what the filter removed, in that one pass.
  4. ↻ ×207K ┅ ×612 under the order_items scan is its loop count, 206,880 runs where the planner budgeted for 612. The band above it is already multiplied out: 3 rows × 206,880 runs, 620,640.
  5. 1.64 s is the scan's self time for its one run. A looped node's time is already the total across its runs: EXPLAIN reports it per loop, and Seqora multiplies it out before drawing the node, the inspector and the time strip.
  6. The result end is 24 rows, once.

The inspector splits the two back out: a Rows per loop ruler, 3 against the planner's 3, on target, and a Loops ruler, 206,880 against 612, 338× more.

Caveats

  • PostgreSQL 17 rounds the per-loop rows to a whole number. A node that found one row in a tenth of its loops prints rows=0.

Last checked against Seqora 0.3.2, PostgreSQL 17 on .

Found a bug, missing a feature, or just curious? Write to me. Every email lands in my own inbox, not a ticket queue.