Skip to content
In this guide

Guide · The plan, node by node

Rows Removed by Filter

Rows Removed by Filter is not a node. It is a line under a scan in EXPLAIN ANALYZE output, counting the rows the node read and then rejected because they failed its Filter. The sample's Seq Scan on commerce.orders reads 12,000,000 rows, keeps 206,880 and prints Rows Removed by Filter: 11793120. Those 11,793,120 rows were fetched, tested against channel = 'web' AND placed_at >= ..., and dropped.

Why it matters

It is the clearest measure of wasted work in a plan. Rows kept over rows read says how well the access path fits the query. orders keeps 1.7%. Under an Index Scan, a Filter line beneath the Index Cond means the index found rows the query then rejected. The rest of the WHERE is checked after the fetch, and a large count there is a missing column. The figure is per loop, so a scan with loops=206880 and Rows Removed by Filter: 2 removed 413,760 rows. Loops has the arithmetic.

What to do

  1. On a Seq Scan removing most of the table, index the filter columns, equality first. The sample's fix is CREATE INDEX orders_channel_placed_idx ON commerce.orders (channel, placed_at DESC).
  2. On an Index Scan with a Filter, add the filtered column to the index so the check moves into the Index Cond.
  3. If the filter is an expression, lower(email) = $1, index the expression: CREATE INDEX ON crm.customers (lower(email)).

In the Plan tab

The removed rows drain off below the scan's band. The drain is marker 3. The rest of the flow is there so you can see what the drain took from.

The row flow of the sample, with the filter's drain under Seq Scan on orders.

The row flow of the sample, with the filter's drain under Seq Scan on orders.

  1. The band into the scan is the 12,000,000 rows it read, every one of them tested against the filter.
  2. 207K ┅ 612 is what survived, 206,880 rows, beside the planner's guess of 612. The band that continues to the right is the 1.7% kept.
  3. −11.8M is Rows Removed by Filter, drained off below the scan: 11,793,120 rows fetched, tested and dropped. A drain this wide against a band this thin is the shape of a missing index.
  4. ↻ ×207K ┅ ×612 is how many times the order_items scan ran. A Rows Removed by Filter on a looped scan is per run, so it would be multiplied by this.
  5. 1.64 s is the scan's self time, most of it spent on rows it then threw away.
  6. The result end, off to the right, is where the rows that survived every filter leave the plan.

The inspector's Rows bar draws read against kept with 2% kept, and the table wheel's guide lists Rows read and Rows kept.

Caveats

  • On a Bitmap Heap Scan the drain adds Rows Removed by Index Recheck to the count.

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.