rudb_native/zones.rs
1//! Showing the planner what a native table already wrote down about itself.
2//!
3//! Every stripe carries the two ends and the null count of every column, in the directory, in
4//! memory from the moment the file is opened. The scan has been reading them since the format
5//! existed and the planner has never seen them, so a query over a native table was ordered from the
6//! same constants a query over a table nobody had measured would get.
7//!
8//! Two things come out of that directory here. [`Stripes`] answers how many rows a set of tests
9//! keeps, which is what a filter's estimate rests on. [`distincts`] answers how many values a column
10//! holds, which is what a join's estimate rests on. They are in one module because they are one
11//! idea, and because TPC-H q05 needs both of them and is the reason either exists.
12//!
13//! [`Common`] came later and is not out of the directory. It answers how many rows hold one
14//! particular value, off the frequency synopsis the writer takes per column, and it belongs here
15//! because it is the same idea pointed at the same reader: a number the file already holds that the
16//! planner was assuming its way past.
17//!
18//! # What q05 actually needed
19//!
20//! The filter is the easy half. The `o_orderdate` range over SF1 keeps 227,597 rows of 1,500,000.
21//! Through Parquet the footer gives 227,556 and through the native file the estimate was 60,000,
22//! which is the constant for a range nobody could read. [`Stripes`] closes that: the same query now
23//! estimates 227,556 from the stripe bounds, off the true answer by forty one rows in two hundred
24//! thousand.
25//!
26//! Closing it changed nothing. q05 measured 5,217 ms with the filter estimate fixed against 4,491
27//! before, which is the same plan and a loaded laptop. The join order was never reading the filter.
28//! It was reading the distinct counts, and the containment assumption in `estimate::matched` only
29//! gives way to `left * right / keys` where both key columns have one. DuckDB writes distinct counts
30//! into a Parquet footer, so the Parquet plan divides the customer against supplier join by the 25
31//! nations and scores it at sixty million, which is enough for the search to put customer against
32//! orders first instead. The native file stated no count for an integer column, the divisor fell
33//! back to the table's own row count, and the same join scored 150,000. So the search took it first
34//! and built the twelve million row intermediate that is the whole of q05's time.
35//!
36//! # Why the stripe and not the part
37//!
38//! A stripe's bounds are in the directory and a part's are a page in the file. The planner is
39//! deciding what to read and reading a page per column per stripe to decide it would be the scan
40//! run twice, so this answers from the stripe alone and never touches the file. The loss is smaller
41//! than it sounds: the interpolation below is what the estimate mostly rests on, and interpolating
42//! inside sixteen stripes and inside nine hundred parts of the same column give nearly the same
43//! fraction when the rows are in no particular order, which is the case this exists for. A part
44//! bound is worth reading when the question is which parts to skip, and that question is the scan's
45//! and is already answered by [`Reader::skips`].
46
47use std::cmp::Ordering;
48
49use rudb_common::Result;
50use rudb_common::Stat;
51use rudb_common::bounds::{Bound, End, Frequencies, Spread, Test, Zones, kept};
52use rudb_common::stat::{Direction, Provenance};
53use rudb_storage::Probe;
54
55use crate::Reader;
56
57/// The bounds of a committed native table, as the planner asks for them.
58///
59/// Holds the reader rather than a copy of the bounds. A reader is a handful of reference counts and
60/// cloning one shares the caches it has already filled, where copying the bounds out would be every
61/// stripe of every column of the table per statement bound.
62#[derive(Debug, Clone)]
63pub struct Stripes {
64 reader: Reader,
65}
66
67impl Stripes {
68 /// The bounds of a table somebody has open.
69 #[must_use]
70 pub fn new(reader: Reader) -> Self {
71 Self { reader }
72 }
73}
74
75impl Zones for Stripes {
76 fn column(&self, name: &str) -> Option<usize> {
77 self.reader.table().fields().iter().position(|field| field.name == name)
78 }
79
80 fn surviving(&self, tests: &[Test]) -> Option<u64> {
81 let probes = probes(tests);
82 let mut total: u64 = 0;
83 for (at, stripe) in self.reader.table().stripes().iter().enumerate() {
84 if self.reader.stripe_skips(at, &probes) {
85 continue;
86 }
87 total = total.checked_add(u64::try_from(stripe.rows()).ok()?)?;
88 }
89 Some(total)
90 }
91
92 fn spread(&self, tests: &[Test]) -> Option<Spread> {
93 let mut passing = 0.0_f64;
94 let mut whole = 0.0_f64;
95 let mut read = 0;
96 for stripe in self.reader.table().stripes() {
97 let rows = rows(stripe.rows());
98 let spread = fraction(tests, stripe.zone());
99 whole += rows;
100 passing += rows * spread.fraction;
101 // The most any one stripe could read rather than a total of them, the same as the
102 // Parquet footer does it and for the same reason: the caller is charging its constant
103 // for the tests nobody answered, so the question is whether anybody answered this one.
104 read = read.max(spread.read);
105 }
106 (read > 0 && whole > 0.0)
107 .then(|| Spread { fraction: (passing / whole).clamp(0.0, 1.0), read })
108 }
109
110 fn extreme(&self, column: usize, end: End) -> Stat<Bound> {
111 // The reader folds the stripes itself and answers only where every one of them wrote a
112 // bound its writer called exact, which is the same promise this has to make. A column whose
113 // ends were widened, or whose stripes do not compare against each other, comes back `None`
114 // there and unknown here.
115 match self.reader.exact_extremes(column) {
116 Ok(Some((low, high))) => {
117 Stat::exact(if end == End::Low { low } else { high }, Provenance::ZoneMap)
118 }
119 _ => Stat::Unknown,
120 }
121 }
122}
123
124/// How many distinct values each column of a native table holds, for the columns it can say.
125///
126/// Two sources, and a column with neither is left out rather than guessed at. An absent column
127/// reads back as unknown and the estimator falls back to the table's row count, which is what every
128/// column did before this existed.
129///
130/// A string column has a global dictionary and the directory records how many codes any row of it
131/// actually holds, so that count is exact and comes back as such. The dictionary page is not opened
132/// to answer, which matters: this runs once per table per statement bound.
133///
134/// Every other column is answered from its two ends, where they are integers. A column of integers
135/// between `low` and `high` cannot hold more than `high - low + 1` distinct values, so the span is a
136/// ceiling, and on the columns that decide a join order it is a tight one. TPC-H nationkey runs 0 to
137/// 24 and holds 25 values, regionkey 0 to 4 and holds 5. On `l_orderkey` the span is six million
138/// against a true one and a half, which is loose and still safe, for the reason below.
139///
140/// # Why a ceiling is the safe end here
141///
142/// The two readers of a distinct count both divide by it. A divisor that is too large makes the
143/// join look smaller, and `estimate::matched` takes the larger of that and the containment
144/// assumption, so too large a span can only fail to raise an estimate and can never lower one below
145/// what shape alone already said. Too small a divisor is the dangerous direction and a span cannot
146/// be too small: a widened bound is wider than the truth, never narrower, so the span it implies is
147/// a ceiling however the bound was written.
148///
149/// A span at or above the table's row count is dropped rather than recorded. The row count is what
150/// the estimator already falls back to for a column nobody counted, so recording it would be an
151/// entry that says what its own absence says.
152///
153/// # Errors
154///
155/// Never, today. The two reads it makes are indexed by a column this loop produced, so neither can
156/// be out of range, and the signature carries the `Result` because both of them do.
157pub fn distincts(reader: &Reader) -> Result<Vec<(String, Stat<u64>)>> {
158 let table = reader.table();
159 let rows = u64::try_from(table.rows()).unwrap_or(u64::MAX);
160 let mut counted = Vec::new();
161 for (at, field) in table.fields().iter().enumerate() {
162 if let Some(exact) = reader.distinct_values(at)? {
163 counted.push((field.name.clone(), Stat::exact(exact, Provenance::Dictionary)));
164 continue;
165 }
166 let Some((Bound::Int(low), Bound::Int(high))) = reader.exact_extremes(at)? else {
167 continue;
168 };
169 let Some(span) = high.checked_sub(low).and_then(|span| u64::try_from(span).ok()) else {
170 continue;
171 };
172 let Some(span) = span.checked_add(1).filter(|&span| span < rows) else {
173 continue;
174 };
175 // The weakest certificate there is: certain from above with the relative error unbounded,
176 // which is the class a ceiling with nothing under it takes everywhere else in the tree.
177 counted.push((
178 field.name.clone(),
179 Stat::certified(span, 1.0, Direction::AtMost, Provenance::ZoneMap),
180 ));
181 }
182 Ok(counted)
183}
184
185/// What a native table's frequency synopsis says about one value, as the planner asks for it.
186///
187/// Holds the reader for the reason [`Stripes`] does. The synopsis is small where it exists at all,
188/// but it exists per column and copying every column's into every plan would be paying for the
189/// columns nothing filters on, which is most of them.
190#[derive(Debug, Clone)]
191pub struct Common {
192 reader: Reader,
193}
194
195impl Common {
196 /// The frequencies of a table somebody has open.
197 #[must_use]
198 pub fn new(reader: Reader) -> Self {
199 Self { reader }
200 }
201}
202
203impl Frequencies for Common {
204 fn column(&self, name: &str) -> Option<usize> {
205 self.reader.table().fields().iter().position(|field| field.name == name)
206 }
207
208 fn rows(&self) -> u64 {
209 u64::try_from(self.reader.table().rows()).unwrap_or(u64::MAX)
210 }
211
212 fn rows_with(&self, column: usize, value: &Bound) -> Stat<u64> {
213 // Only the complete synopsis, by asking for it. A synopsis that dropped anything still says
214 // something useful about the values it kept, but what it says about a value it does not
215 // list is the difference between nothing and a count, and telling those apart is a second
216 // question with a second answer shape. This one is the exact half.
217 let Ok(Some(entries)) = self.reader.exact_frequencies(column) else {
218 return Stat::Unknown;
219 };
220 let mut comparable = false;
221 for (held, count) in entries {
222 // A null entry is the column's nulls, and no equality matches a null. Skipping it is
223 // both the right answer and the only one available, since a null has no bound.
224 let Some(bound) = Bound::of_value(&held) else {
225 continue;
226 };
227 match bound.order(value) {
228 Some(Ordering::Equal) => return Stat::exact(count, Provenance::FrequencySynopsis),
229 Some(_) => comparable = true,
230 None => {}
231 }
232 }
233 // Nothing in the list was the value. That is a count of zero when the list and the constant
234 // were in the same domain, because a complete synopsis accounts for every row. Where not
235 // one entry would even compare, the constant is of another type and the zero would be an
236 // artefact of that rather than a fact about the rows.
237 if comparable { Stat::exact(0, Provenance::FrequencySynopsis) } else { Stat::Unknown }
238 }
239}
240
241/// The tests as the storage layer spells them, which is the same three fields under another name.
242fn probes(tests: &[Test]) -> Vec<Probe> {
243 tests
244 .iter()
245 .map(|test| Probe { column: test.column, op: test.op, value: test.value.clone() })
246 .collect()
247}
248
249/// A stripe's row count as a weight, and zero for one that does not read as a count.
250#[expect(clippy::cast_precision_loss, reason = "a row count is a weight here and not an identity")]
251fn rows(count: usize) -> f64 {
252 count as f64
253}
254
255/// The fraction of one stripe these tests are expected to keep, and how many of them said so.
256///
257/// Tests on one column are intersected by [`kept`] and tests on different columns are multiplied
258/// here, which assumes the columns are independent of each other. That is the assumption the
259/// estimator makes everywhere else and the one that fails first, and a pair of bounds cannot do
260/// anything about it either way.
261///
262/// A stripe this cannot read keeps a fraction of one rather than dropping out of the total. Leaving
263/// it out would report the fraction of the stripes that were read as the fraction of the table.
264fn fraction(tests: &[Test], zone: &rudb_storage::Zone) -> Spread {
265 let mut spread = Spread { fraction: 1.0, read: 0 };
266 for (position, test) in tests.iter().enumerate() {
267 // Once per column rather than once per test, because `kept` is handed every test on the
268 // column and answers for all of them at once. The first mention of a column is the one that
269 // asks and the rest are already in that answer.
270 if tests[..position].iter().any(|earlier| earlier.column == test.column) {
271 continue;
272 }
273 let Some(range) = zone.column(test.column) else { continue };
274 let (Some(low), Some(high)) = (range.low.as_ref(), range.high.as_ref()) else { continue };
275 let Some(kept) = kept(tests, test.column, low, high) else { continue };
276 spread.fraction *= kept.fraction;
277 spread.read += kept.read;
278 }
279 spread
280}