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