rudb_common/stat.rs
1//! What the statistics layer answers with, and how much of it is safe to believe.
2//!
3//! One type comes back from every statistics question, [`Stat`], and it is either a value with a
4//! class attached or it is [`Stat::Unknown`]. `Unknown` is an answer rather than a failure, and it
5//! is not zero, not one and not a default constant. The caller decides what to do without a number,
6//! which is a decision the caller can make well and this layer cannot. A subsystem that invents a
7//! number instead of saying `Unknown` produces plans that are confidently wrong, and the reason
8//! those plans are hard to fix later is that nothing in them records which number was made up.
9//!
10//! Here at rank 0 because the answers are produced at rank 5 by the storage layer, at rank 8 by the
11//! catalog, at rank 11 by the optimizer and at rank 12 by an operator that has just finished its
12//! build side, and consumed in most of the same places. See `spec/stats/04-in-memory.md` section
13//! 4.1, which this module is the implementation of.
14//!
15//! # The class is the point
16//!
17//! [`Class`] is what makes three optimizations legal rather than merely attractive.
18//! `spec/stats/05-every-query.md` section 5.10 states the rule that constant folding on a statistic
19//! is legal only when the statistic is [`Class::Exact`], and [`Stat::exact_value`] is the one way to
20//! ask for a number under that rule. Narrowing a `DECIMAL(15,2)` sum into an `i64` is correct
21//! because the bound is exact and wrong if it is a guess. Seeding a top-n threshold from a quantile
22//! is correct because the certificate says the seed cannot exclude a qualifying row, which is what
23//! [`Class::Certified`] carries and what an estimate does not have.
24//!
25//! [`Class::Estimated`] carries its [`Source`] because `EXPLAIN` prints it, and because a bad plan
26//! is diagnosed by asking which number was wrong and where it came from. A sketch that was merged
27//! badly and a default constant that nobody noticed produce the same wrong row count and want
28//! different fixes.
29//!
30//! # Nothing consumes this yet
31//!
32//! Every producer in the tree still answers `Unknown`, which makes [`Classes`] read all unknown and
33//! makes the ablation of `spec/stats/09-measurement.md` section 9.3 trivially pass. That is G0's
34//! honest zero and it is the number the rest of the series moves.
35
36use std::fmt;
37
38/// One statistic, or the honest absence of one.
39///
40/// `Known` carries the value and how much to trust it. `Unknown` is what a question has no answer
41/// to, and the whole design rests on callers treating it as a case rather than as a zero.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub enum Stat<T> {
44 /// A value, and what kind of knowledge it is.
45 Known {
46 /// The number, bound, flag or set the question asked for.
47 value: T,
48 /// How much of it is known rather than guessed.
49 class: Class,
50 },
51 /// No answer. Not a zero, not a one and not a default.
52 Unknown,
53}
54
55/// How much of an answer is knowledge.
56///
57/// Three cases, ordered by how much they permit. `Exact` permits anything, including changing an
58/// answer by folding a predicate away. `Certified` permits a decision whose fallback survives being
59/// wrong, which is the case a safe bound is for. `Estimated` permits choosing between two plans that
60/// produce the same rows, and nothing else.
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub enum Class {
63 /// The value is the value. A count that was counted, a null count that was maintained, a
64 /// minimum that was compared.
65 Exact,
66 /// The value is wrong by no more than `bound`, as a fraction of itself, and the structure that
67 /// produced it can prove that.
68 ///
69 /// A quantile summary with an epsilon is the usual source. The number is what makes a threshold
70 /// safe to seed from, so a producer that cannot state one should say `Estimated` instead of
71 /// picking a bound that sounds about right.
72 Certified {
73 /// The relative error bound, where `0.01` is one percent.
74 bound: f64,
75 },
76 /// The value is a guess, and this is where it came from.
77 Estimated {
78 /// What produced the guess, because `EXPLAIN` prints it.
79 source: Source,
80 },
81}
82
83/// Where an estimate came from.
84///
85/// Printed by `EXPLAIN`, so these are the words a person reads when a plan went wrong.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum Source {
88 /// A distinct-count sketch, outside the regime where it is exact.
89 Sketch,
90 /// A quantile summary, read without its certificate.
91 Quantile,
92 /// The stored sample.
93 Sample,
94 /// A minimum and a maximum, interpolated between.
95 Zone,
96 /// A dictionary's size standing in for a distinct count.
97 Dictionary,
98 /// A frequency synopsis.
99 Synopsis,
100 /// A propagation rule over another operator's statistics.
101 Propagation,
102 /// Something a previous execution measured, from the observation log.
103 Observation,
104 /// A constant in the source. The weakest answer that is not `Unknown`, and the one worth
105 /// finding in an `EXPLAIN` because it means nobody had a number here at all.
106 Constant,
107}
108
109impl Source {
110 /// The word `EXPLAIN` prints.
111 #[must_use]
112 pub const fn name(self) -> &'static str {
113 match self {
114 Self::Sketch => "sketch",
115 Self::Quantile => "quantile",
116 Self::Sample => "sample",
117 Self::Zone => "zone",
118 Self::Dictionary => "dictionary",
119 Self::Synopsis => "synopsis",
120 Self::Propagation => "propagation",
121 Self::Observation => "observation",
122 Self::Constant => "constant",
123 }
124 }
125}
126
127impl fmt::Display for Source {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 f.write_str(self.name())
130 }
131}
132
133impl Class {
134 /// Whether this is knowledge rather than a guess, which is the test the folding rule applies.
135 #[must_use]
136 pub const fn is_exact(self) -> bool {
137 matches!(self, Self::Exact)
138 }
139
140 /// The class of an answer computed from two others.
141 ///
142 /// Exact combined with anything else is the anything else, which is the honest direction and the
143 /// easy one to get backwards. Two certified bounds add, because a combination of two bounded
144 /// errors is bounded by their sum, and the sum saturates at one because a bound of more than a
145 /// hundred percent says nothing that `Estimated` does not say. Anything involving an estimate is
146 /// an estimate, and the source of a combination is [`Source::Propagation`] unless one of the two
147 /// was a bare constant, in which case the answer is as weak as the constant was.
148 #[must_use]
149 pub fn combine(self, other: Self) -> Self {
150 match (self, other) {
151 (Self::Exact, Self::Exact) => Self::Exact,
152 (Self::Exact, class) | (class, Self::Exact) => class,
153 (Self::Certified { bound: left }, Self::Certified { bound: right }) => {
154 Self::Certified { bound: (left + right).min(1.0) }
155 }
156 (Self::Estimated { source: Source::Constant }, _)
157 | (_, Self::Estimated { source: Source::Constant }) => {
158 Self::Estimated { source: Source::Constant }
159 }
160 _ => Self::Estimated { source: Source::Propagation },
161 }
162 }
163}
164
165impl fmt::Display for Class {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 match self {
168 Self::Exact => f.write_str("exact"),
169 Self::Certified { bound } => write!(f, "certified to {:.2}%", bound * 100.0),
170 Self::Estimated { source } => write!(f, "estimated from {source}"),
171 }
172 }
173}
174
175impl<T> Stat<T> {
176 /// A value that was counted, compared or maintained rather than guessed.
177 pub const fn exact(value: T) -> Self {
178 Self::Known { value, class: Class::Exact }
179 }
180
181 /// A value wrong by no more than `bound` as a fraction of itself.
182 pub const fn certified(value: T, bound: f64) -> Self {
183 Self::Known { value, class: Class::Certified { bound } }
184 }
185
186 /// A guess, and where it came from.
187 pub const fn estimated(value: T, source: Source) -> Self {
188 Self::Known { value, class: Class::Estimated { source } }
189 }
190
191 /// Whether there is an answer at all.
192 #[must_use]
193 pub const fn is_known(&self) -> bool {
194 matches!(self, Self::Known { .. })
195 }
196
197 /// Whether there is no answer.
198 #[must_use]
199 pub const fn is_unknown(&self) -> bool {
200 matches!(self, Self::Unknown)
201 }
202
203 /// The value, whatever its class, for a decision that only chooses between equivalent plans.
204 #[must_use]
205 pub const fn value(&self) -> Option<&T> {
206 match self {
207 Self::Known { value, .. } => Some(value),
208 Self::Unknown => None,
209 }
210 }
211
212 /// The value, but only when it is exact.
213 ///
214 /// The one door for a decision that changes an answer if the number is wrong. Folding a
215 /// predicate away, narrowing arithmetic, dropping an aggregate: all of them ask here, and all of
216 /// them take today's path when the answer is `None`. See `spec/stats/05-every-query.md` section
217 /// 5.10, which is where the rule is stated and where the warning about breaking it in good faith
218 /// is written down.
219 #[must_use]
220 pub const fn exact_value(&self) -> Option<&T> {
221 match self {
222 Self::Known { value, class: Class::Exact } => Some(value),
223 _ => None,
224 }
225 }
226
227 /// How much of the answer is knowledge, or `None` when there is no answer.
228 #[must_use]
229 pub const fn class(&self) -> Option<Class> {
230 match self {
231 Self::Known { class, .. } => Some(*class),
232 Self::Unknown => None,
233 }
234 }
235
236 /// The value, or what the caller decided to do without one.
237 #[must_use]
238 pub fn unwrap_or(self, default: T) -> T {
239 match self {
240 Self::Known { value, .. } => value,
241 Self::Unknown => default,
242 }
243 }
244
245 /// The same answer about a different quantity, with the class carried across unchanged.
246 ///
247 /// For a transformation that cannot lose knowledge, such as reading a row count as a byte count
248 /// through a fixed width. A transformation that does lose knowledge should build its answer with
249 /// the class it deserves rather than mapping.
250 #[must_use]
251 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Stat<U> {
252 match self {
253 Self::Known { value, class } => Stat::Known { value: f(value), class },
254 Self::Unknown => Stat::Unknown,
255 }
256 }
257
258 /// An answer computed from two, unknown when either is unknown, classed by [`Class::combine`].
259 #[must_use]
260 pub fn zip<U, V>(self, other: Stat<U>, f: impl FnOnce(T, U) -> V) -> Stat<V> {
261 match (self, other) {
262 (
263 Self::Known { value: left, class: first },
264 Stat::Known { value: right, class: second },
265 ) => Stat::Known { value: f(left, right), class: first.combine(second) },
266 _ => Stat::Unknown,
267 }
268 }
269}
270
271impl<T> Default for Stat<T> {
272 /// `Unknown`, because a statistic nobody filled in is a statistic nobody knows.
273 fn default() -> Self {
274 Self::Unknown
275 }
276}
277
278impl<T: fmt::Display> fmt::Display for Stat<T> {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 match self {
281 Self::Known { value, class } => write!(f, "{value} ({class})"),
282 Self::Unknown => f.write_str("unknown"),
283 }
284 }
285}
286
287/// How many decisions were made on what.
288///
289/// The class histogram of `spec/stats/09-measurement.md` section 9.5. For a whole suite, the
290/// fraction of the planner's decisions that were exact, certified, estimated or unknown, which is
291/// the direct measurement of whether the statistics layer is doing its job. It is more diagnostic
292/// than q-error for the first several milestones, because early on the estimates are bad for the
293/// boring reason that there are none.
294#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
295pub struct Classes {
296 exact: u64,
297 certified: u64,
298 estimated: u64,
299 unknown: u64,
300}
301
302impl Classes {
303 /// An empty histogram.
304 #[must_use]
305 pub const fn new() -> Self {
306 Self { exact: 0, certified: 0, estimated: 0, unknown: 0 }
307 }
308
309 /// Counts one decision.
310 pub fn record<T>(&mut self, stat: &Stat<T>) {
311 self.record_class(stat.class());
312 }
313
314 /// Counts one decision whose class is already in hand.
315 pub fn record_class(&mut self, class: Option<Class>) {
316 match class {
317 Some(Class::Exact) => self.exact += 1,
318 Some(Class::Certified { .. }) => self.certified += 1,
319 Some(Class::Estimated { .. }) => self.estimated += 1,
320 None => self.unknown += 1,
321 }
322 }
323
324 /// Decisions made on an exact number.
325 #[must_use]
326 pub const fn exact(self) -> u64 {
327 self.exact
328 }
329
330 /// Decisions made on a bounded number.
331 #[must_use]
332 pub const fn certified(self) -> u64 {
333 self.certified
334 }
335
336 /// Decisions made on a guess.
337 #[must_use]
338 pub const fn estimated(self) -> u64 {
339 self.estimated
340 }
341
342 /// Decisions made with no number at all.
343 #[must_use]
344 pub const fn unknown(self) -> u64 {
345 self.unknown
346 }
347
348 /// Every decision counted.
349 #[must_use]
350 pub const fn total(self) -> u64 {
351 self.exact + self.certified + self.estimated + self.unknown
352 }
353
354 /// The fraction of decisions that had a number of any kind behind them, zero for an empty
355 /// histogram.
356 #[must_use]
357 pub fn known_share(self) -> f64 {
358 let total = self.total();
359 if total == 0 {
360 return 0.0;
361 }
362 #[expect(clippy::cast_precision_loss, reason = "a share is a report and not an answer")]
363 {
364 (total - self.unknown) as f64 / total as f64
365 }
366 }
367
368 /// Adds another histogram into this one, for a report that covers a suite rather than a query.
369 pub fn merge(&mut self, other: Self) {
370 self.exact += other.exact;
371 self.certified += other.certified;
372 self.estimated += other.estimated;
373 self.unknown += other.unknown;
374 }
375}
376
377impl fmt::Display for Classes {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 write!(
380 f,
381 "exact {}, certified {}, estimated {}, unknown {}",
382 self.exact, self.certified, self.estimated, self.unknown
383 )
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn unknown_is_the_default() {
393 let stat: Stat<u64> = Stat::default();
394 assert!(stat.is_unknown());
395 assert_eq!(stat.value(), None);
396 assert_eq!(stat.class(), None);
397 assert_eq!(stat.unwrap_or(7), 7);
398 }
399
400 #[test]
401 fn only_an_exact_answer_comes_back_from_exact_value() {
402 assert_eq!(Stat::exact(4_u64).exact_value(), Some(&4));
403 assert_eq!(Stat::certified(4_u64, 0.01).exact_value(), None);
404 assert_eq!(Stat::estimated(4_u64, Source::Sketch).exact_value(), None);
405 assert_eq!(Stat::<u64>::Unknown.exact_value(), None);
406 }
407
408 #[test]
409 fn a_class_degrades_when_it_is_combined() {
410 assert_eq!(Class::Exact.combine(Class::Exact), Class::Exact);
411 assert_eq!(
412 Class::Exact.combine(Class::Estimated { source: Source::Sample }),
413 Class::Estimated { source: Source::Sample }
414 );
415 assert_eq!(
416 Class::Certified { bound: 0.01 }.combine(Class::Certified { bound: 0.02 }),
417 Class::Certified { bound: 0.03 }
418 );
419 assert_eq!(
420 Class::Estimated { source: Source::Sketch }
421 .combine(Class::Estimated { source: Source::Sample }),
422 Class::Estimated { source: Source::Propagation }
423 );
424 assert_eq!(
425 Class::Estimated { source: Source::Sketch }
426 .combine(Class::Estimated { source: Source::Constant }),
427 Class::Estimated { source: Source::Constant }
428 );
429 }
430
431 #[test]
432 fn a_certified_bound_saturates_rather_than_growing_past_everything() {
433 assert_eq!(
434 Class::Certified { bound: 0.8 }.combine(Class::Certified { bound: 0.7 }),
435 Class::Certified { bound: 1.0 }
436 );
437 }
438
439 #[test]
440 fn zip_is_unknown_when_either_side_is() {
441 let known = Stat::exact(10_u64);
442 let unknown = Stat::<u64>::Unknown;
443 assert_eq!(known.zip(unknown, |left, right| left + right), Stat::Unknown);
444 assert_eq!(unknown.zip(known, |left, right| left + right), Stat::Unknown);
445 assert_eq!(known.zip(Stat::exact(5), |left, right| left + right), Stat::exact(15));
446 }
447
448 #[test]
449 fn map_carries_the_class() {
450 let bytes = Stat::certified(100_u64, 0.05).map(|rows| rows * 8);
451 assert_eq!(bytes, Stat::certified(800, 0.05));
452 }
453
454 #[test]
455 fn the_histogram_counts_what_it_was_shown() {
456 let mut classes = Classes::new();
457 classes.record(&Stat::exact(1_u64));
458 classes.record(&Stat::certified(1_u64, 0.1));
459 classes.record(&Stat::estimated(1_u64, Source::Zone));
460 classes.record(&Stat::<u64>::Unknown);
461 assert_eq!(classes.total(), 4);
462 assert_eq!(classes.exact(), 1);
463 assert_eq!(classes.known_share(), 0.75);
464 assert_eq!(classes.to_string(), "exact 1, certified 1, estimated 1, unknown 1");
465
466 let mut all = Classes::new();
467 all.merge(classes);
468 all.merge(classes);
469 assert_eq!(all.total(), 8);
470 }
471
472 #[test]
473 fn an_empty_histogram_knows_nothing_rather_than_everything() {
474 assert_eq!(Classes::new().known_share(), 0.0);
475 assert_eq!(Classes::new().total(), 0);
476 }
477
478 #[test]
479 fn an_answer_prints_its_provenance() {
480 assert_eq!(Stat::exact(12_u64).to_string(), "12 (exact)");
481 assert_eq!(Stat::certified(12_u64, 0.025).to_string(), "12 (certified to 2.50%)");
482 assert_eq!(
483 Stat::estimated(12_u64, Source::Sketch).to_string(),
484 "12 (estimated from sketch)"
485 );
486 assert_eq!(Stat::<u64>::Unknown.to_string(), "unknown");
487 }
488}