Skip to main content

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}