Skip to main content

rudb_kernels/
peel.rs

1//! Answering a predicate once per distinct value rather than once per row.
2//!
3//! A text column read out of a native file arrives as codes into one dictionary that covers the
4//! whole table, and the same dictionary arrives with every chunk of it. So a predicate over that
5//! column asks the same question about the same string over and over. `hits` has a million rows of
6//! `Referer` and about four hundred thousand distinct ones, and every `WHERE Referer <> ''` in
7//! ClickBench compares a million strings to answer four hundred thousand questions.
8//!
9//! Peeling the dictionary off and deciding on the values under it is the name Velox gives this.
10//! DuckDB gets the same effect by letting a dictionary vector travel through an expression instead
11//! of flattening it at the first operator. The two things that make it work here are that the
12//! dictionary is shared as an `Arc`, so the same one is recognised from one chunk to the next by
13//! its pointer, and that the memo is owned by something built once for the query, so it outlives
14//! the chunk that filled it.
15//!
16//! The memo fills lazily rather than in one pass over the dictionary, because a chunk of two
17//! thousand rows points at two thousand codes of four hundred thousand. A whole scan does reach
18//! most of them in the end, but a scan behind a selective filter does not, and an eager pass would
19//! be deciding for values nobody asked about.
20//!
21//! # What may be peeled
22//!
23//! Anything pure and per value. The answer for a code has to depend on the value under that code
24//! and on nothing else, which rules out a predicate that reads the row number or another column.
25//! That is the whole contract and it is the caller's to keep.
26//!
27//! # One peel, one question
28//!
29//! A [`Peel`] holds the answers to one question about one dictionary. The caller owns it from
30//! somewhere that is built per query node, so the question cannot change under it, and the
31//! dictionary is checked by pointer on every chunk so a second dictionary is declined rather than
32//! answered from the first one's memo.
33//!
34//! # When the dictionary is sorted, do not peel at all
35//!
36//! A peel still has to read every distinct value the rows use, and for `Referer` on `hits` that is
37//! four hundred thousand reads out of the file. A dictionary that comes with its sorted order can
38//! do better than that for the one question that matters most, which is whether a value equals a
39//! literal, because the answer is a binary search: about nineteen probes for the whole query, and
40//! after it the filter is an integer compare against one code with no memo to consult.
41//!
42//! [`Lookup`] is that search, memoized the same way and for the same reason. A probe asks the
43//! values how they compare rather than asking them for bytes, which is what lets a format that
44//! keeps the start of each value beside its rank answer nineteen probes out of nineteen without
45//! going near the payload. That matters more than it sounds: the values a binary search lands on
46//! are scattered all over the column, so nineteen reads of them is nineteen different blocks of
47//! the file, which is more than the peel behind a selective filter would have read.
48//!
49//! It only answers equality. A `LIKE`, a regular expression or any other scalar function still
50//! needs the peel, and always will, because no ordering of the values tells you which of them
51//! match a pattern.
52
53use std::sync::atomic::{AtomicU8, Ordering};
54use std::sync::{Arc, OnceLock};
55
56use rudb_common::{Error, Result};
57use rudb_vector::Vector;
58
59/// A memo of one predicate's answers over the values of one dictionary.
60#[derive(Debug, Default)]
61pub(crate) struct Peel {
62    answers: OnceLock<Answers>,
63}
64
65#[derive(Debug)]
66struct Answers {
67    /// The dictionary these answers are about, recognised by pointer rather than by value.
68    dictionary: Arc<Vector>,
69    /// One per dictionary entry: zero for undecided, one for false and two for true.
70    ///
71    /// Atomic because the chunks of one column run on several threads and all of them fill the
72    /// same memo. Relaxed is enough: two threads that decide the same code write the same byte,
73    /// since the predicate is pure, so there is nothing for an ordering to protect.
74    decided: Vec<AtomicU8>,
75}
76
77impl Peel {
78    /// The predicate's answer for every slot, deciding once per distinct code.
79    ///
80    /// `column` is the vector the rows live in, `len` is how many slots the caller wants and `map`
81    /// turns a slot into a row of the column, so this serves both a whole chunk and the rows an
82    /// earlier conjunct kept. `decide` is handed the dictionary and a code and answers for the
83    /// value under it.
84    ///
85    /// `None` means there is nothing to peel and the caller should do what it did before: either
86    /// the column is not a dictionary that shares its values, or this memo was built for a
87    /// different dictionary, which happens when one query node sees two columns.
88    pub(crate) fn answer<M, D>(
89        &self,
90        column: &Vector,
91        len: usize,
92        map: M,
93        decide: D,
94    ) -> Option<Result<Vec<bool>>>
95    where
96        M: Fn(usize) -> usize,
97        D: Fn(&Vector, usize) -> Result<bool>,
98    {
99        let (codes, dictionary) = column.shared_dictionary_parts()?;
100        let answers = self.answers.get_or_init(|| Answers {
101            dictionary: Arc::clone(dictionary),
102            decided: (0..dictionary.len()).map(|_| AtomicU8::new(0)).collect(),
103        });
104        if !Arc::ptr_eq(&answers.dictionary, dictionary) {
105            return None;
106        }
107        Some(answers.run(dictionary, codes, len, map, decide))
108    }
109}
110
111/// Where one literal sits in a dictionary that came with its sorted order.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum Found {
114    /// The dictionary holds the literal under this code, so a row matches exactly when its code is
115    /// this one.
116    At(u32),
117    /// The dictionary does not hold the literal at all, so no row of this column matches it. This
118    /// is the case a whole scan can sometimes be skipped on, and it costs the same search to find.
119    Absent,
120}
121
122/// The result of searching one dictionary for one literal, kept for the life of a query node.
123#[derive(Debug, Default)]
124pub struct Lookup {
125    memo: OnceLock<Searched>,
126}
127
128#[derive(Debug)]
129struct Searched {
130    /// The dictionary this was searched in, recognised by pointer the way [`Peel`] does it.
131    dictionary: Arc<Vector>,
132    found: Found,
133}
134
135impl Lookup {
136    /// Where `wanted` sits in `column`'s dictionary, searched once and remembered.
137    ///
138    /// `None` means there is nothing to search and the caller should do what it did before: the
139    /// column is not a dictionary that shares its values, or the values did not arrive with a
140    /// sorted order, or this was built against a different dictionary.
141    ///
142    /// A failed read is returned rather than remembered, so a caller that retries gets the error
143    /// again rather than a wrong answer cached from a half finished search.
144    pub fn find(&self, column: &Vector, wanted: &[u8]) -> Option<Result<Found>> {
145        let (_, dictionary) = column.shared_dictionary_parts()?;
146        if let Some(memo) = self.memo.get() {
147            return Arc::ptr_eq(&memo.dictionary, dictionary).then_some(Ok(memo.found));
148        }
149        let ranks = dictionary.ranks()?;
150        let found = match search(dictionary, ranks, wanted) {
151            Ok(found) => found,
152            Err(error) => return Some(Err(error)),
153        };
154        // Two threads that get here at once do the same search and set the same answer, and the
155        // one that loses the race drops its own copy of it. Both return what they found.
156        let _ = self.memo.set(Searched { dictionary: Arc::clone(dictionary), found });
157        Some(Ok(found))
158    }
159}
160
161/// The code of `wanted` in a dictionary, by binary search over its sorted order.
162///
163/// The search is here and the comparison is in the source on purpose. What the search does is the
164/// same for every format, and it is short enough to read in one go and to swap for something else.
165/// What one probe costs is entirely up to whoever wrote the file, and a format that keeps the start
166/// of each value beside its rank answers almost every probe without reading a value at all. Asking
167/// the source to compare rather than asking it for a position is what leaves room for that.
168pub(crate) fn search(dictionary: &Vector, ranks: usize, wanted: &[u8]) -> Result<Found> {
169    let mut low = 0;
170    let mut high = ranks;
171    while low < high {
172        let middle = low + (high - low) / 2;
173        match dictionary.compare_rank(middle, wanted)? {
174            std::cmp::Ordering::Less => low = middle + 1,
175            std::cmp::Ordering::Greater => high = middle,
176            std::cmp::Ordering::Equal => {
177                return Ok(Found::At(dictionary.code_at_rank(middle)?));
178            }
179        }
180    }
181    Ok(Found::Absent)
182}
183
184/// How many of a dictionary's values sort before `wanted`, and whether one of them is `wanted`.
185///
186/// [`search`] answers where a literal is and this answers where it would go, which is what an
187/// inequality needs and an equality does not. The search itself belongs to the source rather than to
188/// this crate, because a source that is asked the same question twice is allowed to answer the
189/// second one out of the first, and a loop here could not let it. See [`crate::TextSource::below`].
190///
191/// What the count is for: with `below` values under the literal and `equal` saying whether the
192/// literal itself is in there, a value of rank `r` is under the literal exactly when `r < below` and
193/// under or equal to it exactly when `r < below + equal`. Those two boundaries answer all four
194/// inequalities between them, and neither of them reads a value.
195pub(crate) fn below(dictionary: &Vector, ranks: usize, wanted: &[u8]) -> Result<(usize, bool)> {
196    dictionary.below(ranks, wanted)
197}
198
199impl Answers {
200    fn run<M, D>(
201        &self,
202        dictionary: &Vector,
203        codes: &[u32],
204        len: usize,
205        map: M,
206        decide: D,
207    ) -> Result<Vec<bool>>
208    where
209        M: Fn(usize) -> usize,
210        D: Fn(&Vector, usize) -> Result<bool>,
211    {
212        let mut out = Vec::with_capacity(len);
213        // row at a time: the loop is the point. Every row is one load of its code and one load of
214        // the byte that code was already decided to, and only a code nobody has asked about yet
215        // reaches the predicate.
216        for slot in 0..len {
217            let code = *codes
218                .get(map(slot))
219                .ok_or_else(|| Error::internal("a peeled row is past the end of its codes"))?;
220            let state = self.decided.get(code as usize).ok_or_else(|| {
221                Error::internal("a peeled code is past the end of its dictionary")
222            })?;
223            let mut held = state.load(Ordering::Relaxed);
224            if held == 0 {
225                held = u8::from(decide(dictionary, code as usize)?) + 1;
226                state.store(held, Ordering::Relaxed);
227            }
228            out.push(held == 2);
229        }
230        Ok(out)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use std::sync::atomic::AtomicUsize;
237
238    use rudb_common::{LogicalType, Value};
239
240    use super::*;
241
242    fn letters(values: &[&str]) -> Vector {
243        let values: Vec<Value> = values.iter().map(|text| Value::Varchar((*text).into())).collect();
244        Vector::from_values(LogicalType::Varchar, &values).expect("a vector of text")
245    }
246
247    fn holds(dictionary: &Vector, code: usize) -> Result<bool> {
248        Ok(dictionary.try_bytes_at(code)?.is_some_and(|bytes| bytes.starts_with(b"a")))
249    }
250
251    #[test]
252    fn a_peel_decides_once_per_distinct_code_and_reads_the_memo_after_that() {
253        let values = Arc::new(letters(&["apple", "pear", "avocado"]));
254        let column = Vector::stable_dictionary(vec![0, 1, 2, 1, 0, 0], values).expect("in range");
255        let peel = Peel::default();
256        let calls = AtomicUsize::new(0);
257        let answers = peel
258            .answer(
259                &column,
260                6,
261                |slot| slot,
262                |dictionary, code| {
263                    calls.fetch_add(1, Ordering::Relaxed);
264                    holds(dictionary, code)
265                },
266            )
267            .expect("a shared dictionary is peelable")
268            .expect("the predicate answers");
269        assert_eq!(answers, [true, false, true, false, true, true]);
270        assert_eq!(calls.load(Ordering::Relaxed), 3, "six rows over three distinct values");
271    }
272
273    /// The memo has to survive the chunk, because that is the only thing that makes it worth
274    /// building. A second chunk over the same dictionary asks the predicate nothing.
275    #[test]
276    fn a_second_chunk_over_the_same_dictionary_asks_the_predicate_nothing() {
277        let values = Arc::new(letters(&["apple", "pear"]));
278        let first = Vector::stable_dictionary(vec![0, 1], Arc::clone(&values)).expect("in range");
279        let second = Vector::stable_dictionary(vec![1, 1, 0], values).expect("in range");
280        let peel = Peel::default();
281        let calls = AtomicUsize::new(0);
282        let count = |column: &Vector, len: usize| {
283            peel.answer(
284                column,
285                len,
286                |slot| slot,
287                |dictionary, code| {
288                    calls.fetch_add(1, Ordering::Relaxed);
289                    holds(dictionary, code)
290                },
291            )
292            .expect("peelable")
293            .expect("answers")
294        };
295        assert_eq!(count(&first, 2), [true, false]);
296        assert_eq!(calls.load(Ordering::Relaxed), 2);
297        assert_eq!(count(&second, 3), [false, false, true]);
298        assert_eq!(calls.load(Ordering::Relaxed), 2, "the second chunk decided nothing new");
299    }
300
301    /// One query node that sees a second dictionary declines rather than answering the new codes
302    /// out of the old one's memo, which would be a wrong answer rather than a slow one.
303    #[test]
304    fn a_different_dictionary_is_declined_rather_than_answered_from_the_first_ones_memo() {
305        let peel = Peel::default();
306        let first = Vector::stable_dictionary(vec![0], Arc::new(letters(&["apple"]))).expect("one");
307        let second = Vector::stable_dictionary(vec![0], Arc::new(letters(&["pear"]))).expect("one");
308        assert!(peel.answer(&first, 1, |slot| slot, holds).is_some());
309        assert!(peel.answer(&second, 1, |slot| slot, holds).is_none());
310    }
311
312    #[test]
313    fn a_column_that_is_not_a_shared_dictionary_has_nothing_to_peel() {
314        let flat = letters(&["apple", "pear"]);
315        assert!(Peel::default().answer(&flat, 2, |slot| slot, holds).is_none());
316    }
317
318    /// Stands in for the values of a native column, which are reachable one at a time out of the
319    /// file and which arrive with the sorted order the writer worked out. Counting the reads is
320    /// the point, because the whole claim is that a search does a few of them and a peel does one
321    /// for every distinct value.
322    #[derive(Debug)]
323    struct Filed {
324        values: Vec<Vec<u8>>,
325        /// Codes in sorted value order with the head of each value beside it, which is what the
326        /// native format writes and what lets a probe answer without reading a value.
327        order: Vec<(u64, u32)>,
328        reads: AtomicUsize,
329    }
330
331    /// The first eight bytes of a value as an integer that sorts the way the bytes sort, which is
332    /// what the native format stores per rank.
333    fn head(bytes: &[u8]) -> u64 {
334        let mut word = [0; 8];
335        let take = bytes.len().min(8);
336        word[..take].copy_from_slice(&bytes[..take]);
337        u64::from_be_bytes(word)
338    }
339
340    impl Filed {
341        /// `values` in the order the writer handed out codes, which is not sorted order.
342        fn new(values: &[&str]) -> Self {
343            let values: Vec<Vec<u8>> = values.iter().map(|text| text.as_bytes().to_vec()).collect();
344            let mut order = (0..values.len() as u32)
345                .map(|code| (head(&values[code as usize]), code))
346                .collect::<Vec<_>>();
347            order.sort_by(|&(_, left), &(_, right)| {
348                values[left as usize].cmp(&values[right as usize])
349            });
350            Self { values, order, reads: AtomicUsize::new(0) }
351        }
352
353        fn at(&self, rank: usize) -> Result<(u64, u32)> {
354            self.order
355                .get(rank)
356                .copied()
357                .ok_or_else(|| Error::internal("a rank past the end of the order"))
358        }
359    }
360
361    impl rudb_vector::TextSource for Filed {
362        fn len(&self) -> usize {
363            self.values.len()
364        }
365
366        fn bytes_at(&self, index: usize) -> Result<Option<&[u8]>> {
367            self.reads.fetch_add(1, Ordering::Relaxed);
368            Ok(self.values.get(index).map(Vec::as_slice))
369        }
370
371        fn footprint(&self) -> usize {
372            self.values.iter().map(Vec::len).sum()
373        }
374
375        fn ranks(&self) -> Option<usize> {
376            Some(self.order.len())
377        }
378
379        fn compare_rank(&self, rank: usize, wanted: &[u8]) -> Result<std::cmp::Ordering> {
380            let (found, code) = self.at(rank)?;
381            let settled = found.cmp(&head(wanted));
382            if settled != std::cmp::Ordering::Equal {
383                return Ok(settled);
384            }
385            Ok(self.bytes_at(code as usize)?.unwrap_or_default().cmp(wanted))
386        }
387
388        fn code_at_rank(&self, rank: usize) -> Result<u32> {
389            Ok(self.at(rank)?.1)
390        }
391    }
392
393    /// A dictionary of `count` values named after their position, with codes handed out in an
394    /// order that is nothing like sorted order, plus the source so a test can count its reads.
395    fn filed(count: usize) -> (Vector, Arc<Filed>) {
396        let spellings =
397            (0..count).map(|at| format!("value-{:04}", (at * 7919) % count)).collect::<Vec<_>>();
398        let source =
399            Arc::new(Filed::new(&spellings.iter().map(String::as_str).collect::<Vec<_>>()));
400        let values = Vector::external_text(LogicalType::Varchar, Arc::clone(&source) as Arc<_>)
401            .expect("a filed vector");
402        (values, source)
403    }
404
405    #[test]
406    fn a_literal_is_found_in_a_sorted_dictionary_without_reading_every_value() {
407        let (values, source) = filed(1024);
408        let column = Vector::stable_dictionary(vec![3, 900, 3], Arc::new(values)).expect("codes");
409        let found = Lookup::default()
410            .find(&column, b"value-0700")
411            .expect("a sorted dictionary can be searched")
412            .expect("the search reads");
413        let Found::At(code) = found else { panic!("the dictionary holds it") };
414        assert_eq!(source.values[code as usize], b"value-0700");
415        let reads = source.reads.load(Ordering::Relaxed);
416        assert!(reads <= 11, "a search of 1024 values read {reads} of them");
417    }
418
419    /// The point of writing the start of each value beside its rank. Every probe of this search is
420    /// settled by eight bytes the search already has, so the only value it reads is the one it
421    /// found, and a literal the dictionary does not hold costs no reads at all.
422    #[test]
423    fn a_search_over_values_that_differ_early_reads_only_the_one_it_finds() {
424        let source = Arc::new(Filed::new(&["cherry", "apple", "date", "banana"]));
425        let values = Vector::external_text(LogicalType::Varchar, Arc::clone(&source) as Arc<_>)
426            .expect("a filed vector");
427        let column = Vector::stable_dictionary(vec![0, 1, 2, 3], Arc::new(values)).expect("codes");
428        assert_eq!(
429            Lookup::default().find(&column, b"cherry").expect("searchable").expect("read"),
430            Found::At(0)
431        );
432        assert_eq!(source.reads.load(Ordering::Relaxed), 1, "only the value it found");
433        assert_eq!(
434            Lookup::default().find(&column, b"fig").expect("searchable").expect("read"),
435            Found::Absent
436        );
437        assert_eq!(source.reads.load(Ordering::Relaxed), 1, "and nothing for the one it did not");
438    }
439
440    /// Two values that start the same way cannot be told apart by their first eight bytes, so the
441    /// search has to read them, and the answer has to come out right anyway.
442    #[test]
443    fn values_that_share_their_first_eight_bytes_are_still_told_apart() {
444        let source = Arc::new(Filed::new(&["prefixed-two", "prefixed-one", "prefixed-three"]));
445        let values = Vector::external_text(LogicalType::Varchar, Arc::clone(&source) as Arc<_>)
446            .expect("a filed vector");
447        let column = Vector::stable_dictionary(vec![0, 1, 2], Arc::new(values)).expect("codes");
448        for (wanted, expected) in [
449            (&b"prefixed-one"[..], Found::At(1)),
450            (b"prefixed-two", Found::At(0)),
451            (b"prefixed-three", Found::At(2)),
452            (b"prefixed-four", Found::Absent),
453        ] {
454            assert_eq!(
455                Lookup::default().find(&column, wanted).expect("searchable").expect("read"),
456                expected,
457                "searching for {}",
458                String::from_utf8_lossy(wanted)
459            );
460        }
461    }
462
463    #[test]
464    fn a_literal_the_dictionary_does_not_hold_is_answered_absent() {
465        let (values, _) = filed(64);
466        let column = Vector::stable_dictionary(vec![0], Arc::new(values)).expect("codes");
467        let found = Lookup::default().find(&column, b"nothing").expect("searchable").expect("read");
468        assert_eq!(found, Found::Absent);
469    }
470
471    /// The empty string is the literal ten ClickBench queries filter on, and it is the one value
472    /// most likely to sit at rank zero, so it is worth naming as its own case.
473    #[test]
474    fn the_empty_string_is_found_like_any_other_value() {
475        let source = Arc::new(Filed::new(&["pear", "", "apple"]));
476        let values = Vector::external_text(LogicalType::Varchar, source).expect("a filed vector");
477        let column = Vector::stable_dictionary(vec![0, 1, 2], Arc::new(values)).expect("codes");
478        assert_eq!(
479            Lookup::default().find(&column, b"").expect("searchable").expect("read"),
480            Found::At(1)
481        );
482    }
483
484    #[test]
485    fn a_second_chunk_over_the_same_dictionary_searches_nothing() {
486        let (values, source) = filed(256);
487        let values = Arc::new(values);
488        let first = Vector::stable_dictionary(vec![0, 1], Arc::clone(&values)).expect("codes");
489        let second = Vector::stable_dictionary(vec![2], Arc::clone(&values)).expect("codes");
490        let lookup = Lookup::default();
491        let found = lookup.find(&first, b"value-0100").expect("searchable").expect("read");
492        let reads = source.reads.load(Ordering::Relaxed);
493        assert!(reads > 0, "the first chunk did the search");
494        assert_eq!(lookup.find(&second, b"value-0100").expect("searchable").expect("read"), found);
495        assert_eq!(source.reads.load(Ordering::Relaxed), reads, "the second chunk read nothing");
496    }
497
498    /// Whatever a dictionary knows about itself, a second one is a different question, and
499    /// answering it from the first one's search would be wrong rather than slow.
500    #[test]
501    fn a_search_is_not_reused_across_two_dictionaries() {
502        let lookup = Lookup::default();
503        let (first, _) = filed(8);
504        let (second, _) = filed(8);
505        let first = Vector::stable_dictionary(vec![0], Arc::new(first)).expect("codes");
506        let second = Vector::stable_dictionary(vec![0], Arc::new(second)).expect("codes");
507        assert!(lookup.find(&first, b"value-0000").is_some());
508        assert!(lookup.find(&second, b"value-0000").is_none());
509    }
510
511    /// A dictionary built in memory does not know its own order, and the caller has to be left on
512    /// the path it would have taken, which is the peel.
513    #[test]
514    fn a_dictionary_with_no_order_is_declined() {
515        let values = Arc::new(letters(&["apple", "pear"]));
516        let column = Vector::stable_dictionary(vec![0, 1], values).expect("codes");
517        assert!(Lookup::default().find(&column, b"apple").is_none());
518    }
519}