inillucent_sql/cost.rs
1//! What a plan costs, and which order to visit the FROM terms in.
2//!
3//! Invariant: cost is measured in the same currency SQLite measures it in - the
4//! base-2 logarithm of the number of rows a step touches, scaled - so that a
5//! plan comparison means the same thing in both engines. That is not a stylistic
6//! choice. The acceptance test for this phase compares which *plan* each engine
7//! picks, and two engines using different currencies disagree on ties for
8//! reasons that are not defects.
9//!
10//! Absent statistics the numbers are SQLite's own guesses: a table holds about
11//! a million rows, an equality selects a tenth of them, a range a quarter, and a
12//! unique index exactly one. Those defaults are what make an unanalysed plan
13//! match the reference's, so they are written out here rather than being
14//! whatever seemed reasonable.
15
16/// The row count assumed for a table nothing has measured.
17///
18/// SQLite's `SQLITE_DEFAULT_ROWEST`. It is deliberately large: with a small
19/// guess every index looks pointless, and the plan for an unanalysed schema
20/// would be a full scan of everything.
21pub const DEFAULT_ROWS: f64 = 1_048_576.0;
22
23/// The share of a table an equality on an indexed column is assumed to select.
24///
25/// Used only where no index is involved. An equality *on an index* is estimated
26/// by [`default_equality_rows`] instead, which is an absolute count rather than
27/// a share - see the note there for why the difference matters.
28pub const EQUALITY_SHARE: f64 = 10.0;
29
30/// How many rows an equality on an unmeasured index is assumed to match.
31///
32/// SQLite's `sqlite3DefaultRowEst` fills an unanalysed index's estimates with
33/// these, as LogEst 33, 32, 30, 28, 26, 23 - about twenty rows for the first
34/// equality column, falling to ten and staying there. They are *counts*, not
35/// fractions of the table, and that distinction is the whole point: somebody
36/// who indexed a column and then compared it for equality was pinning down a
37/// row, not selecting a tenth of the table, and the bigger the table the more
38/// true that is.
39///
40/// Getting this wrong is not a rounding error, it reverses join orders. With a
41/// tenth-of-the-table estimate the planner priced a seek into a 25,000 row
42/// table at 2,500 rows, decided the seek was not worth it, and scanned that
43/// table once per outer row instead - which took a benchmark round from seconds
44/// to the better part of an hour, and put a `SCAN` where the reference had a
45/// `SEARCH ... USING INDEX`.
46const DEFAULT_EQUALITY_ROWS: [f64; 6] = [20.0, 18.0, 15.0, 13.0, 11.0, 10.0];
47
48/// Returns how many rows an equality on an unmeasured index is assumed to match.
49///
50/// Never more than the table holds: a two-row table cannot return twenty.
51/// @param equalities - how many leading index columns the search pins down
52/// @param rows - how many rows the table is estimated to hold
53pub fn default_equality_rows(equalities: usize, rows: f64) -> f64 {
54 let at = equalities.max(1).saturating_sub(1);
55 let estimate = DEFAULT_EQUALITY_ROWS
56 .get(at)
57 .copied()
58 .unwrap_or_else(|| DEFAULT_EQUALITY_ROWS.last().copied().unwrap_or(10.0));
59 estimate.min(rows.max(1.0))
60}
61
62/// The share a range on an indexed column is assumed to select.
63pub const RANGE_SHARE: f64 = 4.0;
64
65/// What it costs to fetch a table row once an index has found its key.
66///
67/// A second descent of a second B-tree, so it is charged per matching row and
68/// is what makes a covering index worth having.
69pub const FETCH_PENALTY: f64 = 3.0;
70
71/// What sorting a row costs relative to visiting one.
72pub const SORT_FACTOR: f64 = 3.0;
73
74/// Returns how much of a row's width one index entry is.
75///
76/// An entry holds the indexed columns and the row's key; a row holds every
77/// column. Cost here is bytes touched, so the ratio of the two is what a
78/// covering path saves over reading the rows - and it is what makes a covering
79/// scan of a narrow index beat a scan of a wide table when neither has a
80/// predicate to narrow it.
81///
82/// The floor stops a one-column index over a fifty-column table from looking
83/// fifty times cheaper than it is: pages, not just bytes, are what a scan
84/// touches, and a b-tree of any width has a per-entry cost that does not shrink
85/// with the entry.
86/// @param index_columns - how many columns the index is over
87/// @param table_columns - how many the table has
88pub fn entry_share(index_columns: usize, table_columns: usize) -> f64 {
89 let entry = index_columns.saturating_add(1) as f64;
90 let row = table_columns.max(1) as f64;
91 (entry / row).clamp(ENTRY_SHARE_FLOOR, 1.0)
92}
93
94/// The least a covering entry is allowed to be worth relative to a row.
95pub const ENTRY_SHARE_FLOOR: f64 = 0.25;
96
97/// Returns the estimated cost of visiting a number of rows through a scan.
98///
99/// One visit per row, and no descent per row: a scan walks the leaves in order
100/// and each step is to the next entry rather than from the root. Charging it a
101/// descent per row - the obvious reading of "reading a row costs a descent" -
102/// makes a scan of a thousand rows cost ten thousand, and then an index search
103/// that returns *every* row of the table still looks cheaper than reading the
104/// table. Nothing would ever choose a scan again.
105pub fn scan_cost(rows: f64) -> f64 {
106 rows.max(1.0)
107}
108
109/// Returns the estimated cost of a search that returns some of the rows.
110///
111/// One descent to find the first match, then one visit per match, plus a second
112/// descent per match when the index does not carry the columns the query wants.
113pub fn search_cost(rows: f64, matches: f64, covering: bool) -> f64 {
114 let rows = rows.max(1.0);
115 let matches = matches.max(1.0);
116 let fetch = if covering { 0.0 } else { FETCH_PENALTY };
117 log2(rows) + matches * (1.0 + fetch)
118}
119
120/// Returns the estimated cost of sorting a number of rows.
121///
122/// This one *is* `n log n`, because a sort really does compare each row against
123/// a logarithmic number of others.
124pub fn sort_cost(rows: f64) -> f64 {
125 let rows = rows.max(1.0);
126 rows * log2(rows) * SORT_FACTOR
127}
128
129/// Returns a base-2 logarithm that never goes below one.
130///
131/// A B-tree of one row still costs a descent, and a cost of zero would make a
132/// search over an empty table free - which is how a planner ends up preferring
133/// a path over a table it has not measured to one it has.
134pub fn log2(rows: f64) -> f64 {
135 rows.max(2.0).log2()
136}
137
138/// Returns SQLite's estimate of how wide one column's values are.
139///
140/// SQLite's `szEst`: 1 for a column with no declared type and for every numeric
141/// affinity, 16 for a `TEXT` or `BLOB` declaration, and for `CHAR(n)`-style
142/// declarations `n / 4 + 1`, at most 255. Only the comparison between a table
143/// and an index matters, so the numbers are the ones SQLite compares.
144///
145/// @param column - the column as declared
146pub fn column_width(column: &crate::catalog_view::ColumnInfo) -> u32 {
147 use inillucent_value::Affinity;
148 if column.declared_type.is_empty() {
149 return 1;
150 }
151 if !matches!(column.affinity, Affinity::Text | Affinity::Blob) {
152 return 1;
153 }
154 let lowered = column.declared_type.to_ascii_lowercase();
155 let Some(start) = lowered.windows(4).position(|word| word == b"char") else {
156 return 16;
157 };
158 let digits: Vec<u8> = lowered
159 .iter()
160 .skip(start)
161 .skip_while(|byte| !byte.is_ascii_digit())
162 .take_while(|byte| byte.is_ascii_digit())
163 .copied()
164 .collect();
165 let length = String::from_utf8_lossy(&digits).parse::<u32>().unwrap_or(0);
166 (length / 4 + 1).clamp(1, 255)
167}
168
169/// Reports whether an index entry is narrower than a row of its table, which is
170/// the only reason SQLite walks an index in place of the table when nothing
171/// else decides.
172///
173/// **A covering index that is no narrower is not walked.** `SELECT * FROM t` on
174/// `t(id INTEGER PRIMARY KEY, u UNIQUE)` returns the rows in rowid order in
175/// SQLite, because the index entry (`u` and the rowid) is as wide as the row.
176/// Walking it anyway returned them in `u` order, and an application that reads
177/// a table without an `ORDER BY` gets rowid order.
178///
179/// @param table - the table
180/// @param index - an index over it that holds every column the query reads
181pub fn index_is_narrower(
182 table: &crate::catalog_view::TableInfo,
183 index: &crate::catalog_view::IndexInfo,
184) -> bool {
185 if table.without_rowid {
186 return true;
187 }
188 let row: u32 = table
189 .columns
190 .iter()
191 .map(column_width)
192 .sum::<u32>()
193 .saturating_add(u32::from(table.rowid_alias.is_none()));
194 let entry: u32 = index
195 .columns
196 .iter()
197 .map(|key| {
198 key.plain_column()
199 .and_then(|at| table.columns.get(usize::from(at)))
200 .map_or(1, column_width)
201 })
202 .sum::<u32>()
203 .saturating_add(1);
204 entry < row
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 /// A search that returns one row of a million beats a scan, and by a lot.
212 #[test]
213 fn a_selective_search_beats_a_scan() {
214 let rows = 1_000_000.0;
215 assert!(search_cost(rows, 1.0, false) < scan_cost(rows) / 1000.0);
216 }
217
218 /// A search that returns every row does not - it pays a second descent per
219 /// row for the privilege of reading the same table.
220 #[test]
221 fn an_unselective_search_does_not() {
222 let rows = 1_000.0;
223 assert!(search_cost(rows, rows, false) > scan_cost(rows));
224 }
225
226 /// The crossover is where it should be: a search that returns a fifth of a
227 /// table is still worth it, and one that returns half is not.
228 #[test]
229 fn the_crossover_is_a_fraction_of_the_table() {
230 let rows = 10_000.0;
231 assert!(search_cost(rows, rows / 5.0, false) < scan_cost(rows));
232 assert!(search_cost(rows, rows / 2.0, false) > scan_cost(rows));
233 }
234
235 /// A covering index is cheaper than the same search that has to fetch.
236 #[test]
237 fn covering_is_cheaper_than_fetching() {
238 assert!(search_cost(1_000.0, 100.0, true) < search_cost(1_000.0, 100.0, false));
239 }
240
241 /// An empty table still costs a descent, so nothing is free.
242 #[test]
243 fn nothing_costs_nothing() {
244 assert!(scan_cost(0.0) > 0.0);
245 assert!(search_cost(0.0, 0.0, true) > 0.0);
246 }
247}