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//! # 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}