Basis Lab

Answers · Where the data lives

Why does the planner underestimate rows when I filter on two columns that are related?

In shortIt multiplies the two filter shares as if the columns were unrelated, so when one column already implies the other the estimate comes out far too low.

The planner treats the two filters as independent, so it multiplies their shares even when one column already implies the other. That makes the estimate too low by the ratio between the share that passes the second filter after the first has been applied and the share that passes it on its own.

m^=n⋅a⋅bm=n⋅a⋅c\hat{m}=n\cdot a\cdot b \qquad m=n\cdot a\cdot c

What the multiplication assumes

Here nn is the table row count, aa is the share passing the first filter, bb is the share passing the second filter measured over the whole table, and cc is the share passing the second filter among rows that already passed the first. The estimate m^\hat{m} uses bb. The true count mm uses cc. Multiplying aa and bb is exactly the rule for independent events: it says the second filter keeps the same fraction whether or not the first has been applied. Nothing in a per-column histogram records any relationship between the columns, so the planner has no other rule available.

Take a table of 9,400,000 addresses. The city filter keeps 3 per cent, so 9,400,000×0.03=282,0009{,}400{,}000 \times 0.03 = 282{,}000 rows. The postcode prefix filter keeps 2 per cent on its own. The planner multiplies the shares: 0.03×0.02=0.00060.03 \times 0.02 = 0.0006, and 9,400,000×0.0006=5,6409{,}400{,}000 \times 0.0006 = 5{,}640 rows. In the data, nearly every row with that prefix is in that city, so the second filter removes almost nothing from the 282,000. If it leaves 274,000, the true count is 274,000 and the estimate is low by 274,000÷5,640≈48.6274{,}000 \div 5{,}640 \approx 48.6, nearly fifty times. Equivalently c≈274,000÷282,000≈0.97c \approx 274{,}000 \div 282{,}000 \approx 0.97, not 0.02.

FIG 1The same two filters, estimated and then counted
stepstepwhat happensrowswhat happened
1table size from the summarythe starting point9400000No error here. The row count is one of the few things the summary knows directly.
2apply the city filter3 per cent of 9,400,000282000A good estimate, from a measured frequency rather than an assumption.
3apply the postcode filter as the planner doesmultiply by 2 per cent56400.03 times 0.02 is 0.0006, and 9,400,000 times 0.0006 is 5,640.
4apply the postcode filter as the data doesit removes almost nothing274000Nearly every row that passed the city filter passes this one too, because the two columns are saying the same thing.
5the plan built on 5,640 rowsa nested loop, two further joins above274000274,000 divided by 5,640 is about 48.6, so the method chosen above is fed nearly fifty times more rows than it was chosen for.
5 steps
The only wrong step is the multiplication by the unconditional second share. Everything before it is a measurement, and everything after it is a consequence.

Why the natural guess fails

The natural guess is that because each column's frequency was measured, combining them is just arithmetic. It is arithmetic, but the second frequency was measured over the whole table, not over the rows that survived the first filter. The planner needs the conditional share cc, and it does not have it. When the columns are related, cc is larger than bb, often much larger. The error is not in the 3 per cent or the 2 per cent as separate facts; it is in using the unconditional 2 per cent where the conditional share is what the query asks for.

The multiplication is exact when the columns are unrelated, because then c=bc = b. It is nearly exact for weak relationships. It fails badly for a functional dependency, where one column determines another: a postcode determines its city, a product identifier determines its category. Then c=1c = 1, the second filter removes nothing once the first has been applied, and the estimate is low by 1/b1/b. With b=0.02b = 0.02 that is a factor of 50; with b=0.01b = 0.01 it is 100. A selective second filter makes the factor large. A second filter keeping 20 per cent on its own can be out by only five in the same extreme case, because 1÷0.2=51 \div 0.2 = 5.

What changes it

The direct fix is to tell the database that the columns are related, so its statistics job measures their joint distribution and the estimate uses the measured cc rather than assuming bb. Most systems support this as a declaration over a group of columns. The quick fix is to remove the redundant filter: if the postcode filter removes nothing after the city filter, deleting it costs no correctness and removes the bad multiplication. Where neither is possible, the plan is being chosen on an estimate that cannot be repaired from the query alone, and the remaining options constrain the planner rather than inform it.

Worth keeping

  • The planner multiplies filter shares because a per-column histogram records no relationship between columns.
  • When one column determines another, the second filter removes nothing after the first, so the estimate is low by one over the second filter's share.
  • Joint statistics or deleting the redundant filter fixes the cause; forcing a plan only works around it.

Where this goes next

This question is one step of What the Planner Decides, a course that works through its subject from the beginning. The page that teaches this part of it is Columns That Are Not Independent, and it reads the whole way through whether or not you have an account.

What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.

Read that pageThe whole course

Other questions people ask about this

  1. How does the database estimate how many rows a WHERE clause will return?It does not scan the table: it uses stored summary statistics to turn each filter into a fraction of rows and multiplies the fractions.
  2. If SQL is declarative, who decides how the join actually gets run?The database's query planner chooses the join method and order; your SQL only says which rows the answer must contain.
  3. Why does the order I join tables in matter if joins are commutative?The final result is the same, but the rows carried between joins are not, and that is what the work is paid on.
  4. Will refreshing statistics fix a bad query plan?Only if the summary was taken before the data changed; otherwise refreshing re-measures the same assumptions and the plan stays wrong.
  5. Why is the date off by one day after I save it and read it back?You saved a calendar date as if it were a precise moment, so showing it in another time zone can move it to the day before or after.
  6. Is UTF-8 the same thing as Unicode, or what's the difference?No: Unicode assigns a number to each character, while UTF-8 is a way of writing those numbers as bytes, using one to four bytes per character.

Read alongside