Selectivity, Cost and Choosing a Scan
Estimate how many rows each plan keeps, price it in page reads, and take the cheapest
On a table with statistics, estimate the rows of every plan node from distinct counts and min and max, price each candidate scan with integer formulas, choose the cheapest, and print rows= and cost= on every node; on a table without statistics, keep the rules.
- Selectivity: the fraction of rows a predicate keeps, guessed from a few numbers per column
- Independent predicates multiply, which is often wrong and always simple
- Cost counts page reads: a full scan reads each page once, an index pays a table descent per row it finds
- An index loses when it finds too many rows; a covering index loses much later
- The planner believes its statistics, however stale: a wrong estimate makes a wrong plan
Why this stage exists
Every plan so far was chosen by rule: if an index can serve the WHERE, use it. Here is a table where that rule is a disaster. A thousand rows, a column v with two values, 'a' and 'b', and an index on v:
select count(pad) from w where v = 'a';
The index finds 500 entries, and each one costs a descent of the table: hundreds of page reads, many of the same pages again and again. A full scan reads each of the table's pages once. The rule picked the slow plan, and it will keep picking it.
A cost-based optimizer asks two questions instead: how many rows will each plan touch, and what will that cost. The statistics of the previous stage answer the first; a few integer formulas answer the second.
How many rows: selectivity
The selectivity of a predicate is the fraction of rows it keeps. With statistics it is a guess, but a good one:
| Predicate | Guess | Over 1000 rows, ages 0–99, ten cities |
|---|---|---|
city = 'c1' | one in ndv | 1/10 → 100 |
age < 5 | the share of the range below 5 | 5/100 → 50 |
age >= 90 | the share at or above 90 | 10/100 → 100 |
age < 5 and city = 'c1' | the product | 5/100 × 1/10 → 5 |
city > 'c5' | no good guess for strings | 1/3 → 333 |
Multiplying assumes the two predicates are independent: that knowing a person's city tells you nothing about their age. Real data breaks that all the time (city and postcode, make and model), and real optimizers misestimate because of it. It is still what almost every optimizer does by default, because anything better needs statistics on column pairs.
What it costs: page reads
| Scan | Cost | Why |
|---|---|---|
| SeqScan | P | every page of the table, once |
| IndexScan of an index | Di + E × Dt | one descent of the index, then one of the table per row found |
| IndexOnlyScan | Di + ceil(E / 100) | one descent, then entries side by side, about a hundred to a leaf |
| IndexScan of the table's key | Dt + ceil(E × P / R) | one descent, then the pages those rows fill |
Di and Dt are the depths of the index and the table, P and R the table's pages and rows, E the estimated rows. With them the two-value table is no contest:
Change the query to select count(v) and the index covers it. Now the index-only cost is about 2 + 5, far below 110, and the index wins again. Covering is not a detail: it moves the tipping point by a factor of a hundred.
On a tiny table the opposite surprise happens. Four rows in one leaf: a full scan costs 1, any index costs at least 2. So where name = 'bob' is a SeqScan even with an index on name, and that is right.
Stale statistics are believed
ANALYZE runs only when asked, so the planner may be working from an old picture. Take 1000 distinct values, ANALYZE, then update t set v = 'x'. The statistics still say one row in a thousand has any given value, so where v = 'x' is estimated at 1 row and planned as an IndexScan. It finds 1000, with 1000 table descents. Nothing is wrong with the planner; its information is. ANALYZE again and the same query becomes a full scan. The next stage, EXPLAIN ANALYZE, puts the estimate and the truth side by side so you can see this happen.
No statistics, no change
A table that has never been analysed is planned exactly as before, by the rules, and explain prints no estimates. That keeps every earlier stage's plans as they were, and makes the cost model something you turn on, table by table, by running ANALYZE.
The algorithm in words
Estimate rows
Every conjunct gets a fraction; a node's rows are the table's rows times the fractions of every conjunct at or below it.
- 1.c = v: 1 / ndv of c.
- 2.c < v on an INT column: (v − min) / (max − min + 1), and likewise for <=, > and >=, clamped to 0..1.
- 3.c is null: nulls / rows; c is not null: the rest.
- 4.Anything else: one third.
- 5.Multiply the fractions exactly, multiply by the table's rows, and round down once at the end.
Price each scan
Costs are page reads, in integers.
- 1.SeqScan: the table's pages.
- 2.IndexScan of an index: the index's depth, plus the estimated rows times the table's depth, since each row found is one more descent of the table.
- 3.IndexOnlyScan: the index's depth plus the estimated rows divided by 100, rounded up: a leaf holds many entries.
- 4.IndexScan of the table's own tree: its depth, plus the pages the estimated rows fill.
- 5.Every other node costs what its child costs; Project and Sort keep their child's rows.
Choose
Rules first where they cannot be wrong, cost everywhere else.
- 1.A table without statistics: plan by the rules of the earlier stages, and print no estimates.
- 2.A key bound on the table's key: the table's IndexScan, as always.
- 3.Otherwise price a SeqScan and the scan each usable index would give, index-only where it covers the statement, and take the cheapest; ties go to the SeqScan.
- 4.Print (rows=E cost=C) after every node.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, section 60.
| Input | Output |
|---|---|
explain select * from t where v = 'c7'; (1000 distinct, analysed) | [PLAN] └── IndexScan (table=t, index=by_v, key='c7') (rows=1 cost=<C>) |
explain select * from w where v = 'a'; (two values, analysed) | Filter (v = 'a') (rows=500 cost=<P>) over SeqScan (table=w) (rows=1000 cost=<P>) |
explain select * from a where age < 5 and city = 'c1'; | Filter ((age < 5) AND (city = 'c1')) (rows=5 cost=<P>) |
explain select * from s where name = 'bob'; (four rows, one leaf) | Filter (name = 'bob') (rows=1 cost=1) over SeqScan (table=s) (rows=4 cost=1) |
explain select * from s where id = 2; | IndexScan (table=s, key=2) (rows=1 cost=2) |
| any table never analysed | the plan of the earlier stages, with no (rows= cost=) |
There is no new syntax and no new code.
What you should see when done
$droid --db cost.dbWelcome to droid!>create table s (id int, name varchar(10), n int);Create table statement executed successfully>create index sn on s (name);Create index statement executed successfully>insert into s (id, name, n) values (1, 'ada', 1);Insert statement executed successfully (1 rows)>insert into s (id, name, n) values (2, 'bob', 2);Insert statement executed successfully (1 rows)>explain select * from s where name = 'bob';[PLAN] select * from s where name = 'bob'[PLAN] └── IndexScan (table=s, index=sn, key='bob')>analyze s;Analyze statement executed successfully>explain select * from s where name = 'bob';[PLAN] select * from s where name = 'bob'[PLAN] └── Filter (name = 'bob') (rows=1 cost=1)[PLAN] └── SeqScan (table=s) (rows=2 cost=1)>.exitexiting.. good bye!
Before ANALYZE the rule uses the index; after it, the planner knows the table is one page and reads it whole.
Common bugs in this stage
- Rounding each factor.
5/100 × 1/10 × 1000is 5; rounding5/100 × 1000to 50 first and then50 × 1/10happens to agree, but other inputs do not. Multiply exact fractions and round down once. - Floating point. Two languages can print different estimates from the same doubles. Use integers or exact fractions.
- Forgetting the table descent per row. Without
E × Dtan index always looks cheaper than a scan, and the stage teaches nothing. - Choosing by cost on a table without statistics. The earlier stages' plans must not change.
- Clamping too late.
age < 500over ages 0–99 is a selectivity of 1, not 5.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: keep a fraction as two
uint64_t(numerator and denominator); a few conjuncts over a thousand rows stay far from overflow. - C++: the same pair, or
std::ratio-like helpers; avoiddouble. - Rust: a small
struct Frac { num: u128, den: u128 }withMulkeeps the code readable. - Zig:
u128numerators and denominators, multiplied withstd.math.multo catch overflow in debug builds.
Checklist
One line per thing the tests check. The tags name the test cases.
- One row in a thousand goes through the index; half the table is read by a full scan, unless the index covers itselective-and-unselective
- Stale statistics keep a bad plan; ANALYZE fixes it; a key bound reads the key either wayplan-flips-with-data
- explain prints rows= and cost= on every node of an analysed table, and the cheaper scanestimates-and-choice
- Ranges are estimated from min and max, equalities from distinct counts, and AND multiplies themselectivity
- On one leaf, every cost is computed by hand, and the full scan winshand-computed-cost