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.
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.
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.
| 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.
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.
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.
Every conjunct gets a fraction; a node's rows are the table's rows times the fractions of every conjunct at or below it.
Costs are page reads, in integers.
Rules first where they cannot be wrong, cost everywhere else.
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.
$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.
5/100 × 1/10 × 1000 is 5; rounding 5/100 × 1000 to 50 first and then 50 × 1/10 happens to agree, but other inputs do not. Multiply exact fractions and round down once.E × Dt an index always looks cheaper than a scan, and the stage teaches nothing.age < 500 over ages 0–99 is a selectivity of 1, not 5.The page above is the same for every language. These are the only language-specific hints.
uint64_t (numerator and denominator); a few conjuncts over a thousand rows stay far from overflow.std::ratio-like helpers; avoid double.struct Frac { num: u128, den: u128 } with Mul keeps the code readable.u128 numerators and denominators, multiplied with std.math.mul to catch overflow in debug builds.