Skip to main content

rudb_opt/
lib.rs

1//! The rewrite passes, cardinality estimation, join ordering, predicate transfer and layout adaptation.
2//!
3//! Rank 11 in the layer rule. See `xtask/layers.toml` and `spec/18-package-layout.md`.
4//!
5//! Six passes so far. `spec/09-optimizer.md` section 9.1 describes a sequence and [`PASSES`] is
6//! the start of it. Column pruning came first, because it is the pass whose absence is measured in
7//! gigabytes: a scan that reads 105 columns to answer a question about three is the whole of the
8//! difference on ClickBench, and the Parquet reader has been able to read a subset since M1 with
9//! nothing able to tell it which subset.
10
11#![forbid(unsafe_code)]
12
13pub mod columns;
14pub mod empty;
15pub mod estimate;
16pub mod explain;
17pub mod filter;
18pub mod fold;
19pub mod late;
20pub mod limit;
21pub mod nulls;
22pub mod pass;
23pub mod tables;
24pub mod topn;
25mod transitive;
26mod walk;
27
28use rudb_common::{Error, Result};
29use rudb_plan::{Node, NodeRef, Plan};
30
31use crate::pass::{Context, Pass};
32
33/// The crate this rank belongs to, so that the layer check has something to read.
34pub const RANK: u8 = 11;
35
36/// The passes, in the order they run.
37///
38/// A fixed sequence rather than a loop to a fixed point, which is what `spec/09-optimizer.md`
39/// section 9.1 asks for and what DuckDB does. A fixed point is easy to write and hard to bound: a
40/// pair of passes that undo each other runs forever, and the version that stops after a few rounds
41/// has a plan that depends on how many rounds it was given.
42///
43/// Folding is before pruning because folding removes column references and pruning drops the columns
44/// nothing refers to, so a `CASE WHEN false THEN t.a ELSE 1 END` costs a column read when the two run
45/// the other way around. Nothing in the other direction is given up: pruning drops columns and
46/// renumbers bindings, and neither of those makes anything foldable.
47///
48/// Filter pushdown goes between them. After folding, because a predicate that folds to a constant is
49/// a predicate with nothing to push and the pass that moves it should not be the one that finds out.
50/// Before pruning, because moving a filter below a projection rewrites it in terms of columns the
51/// projection reads, and pruning has to see the plan after the move or it drops a column that
52/// something now refers to.
53///
54/// Empty result pullup is after filter pushdown, because pushdown is what moves an unsatisfiable
55/// predicate down to the scan it should stop and what drops the conjuncts that were always true, so
56/// the pass that looks for a predicate nothing can satisfy should look after that has happened. It
57/// is before pruning for the same reason folding is: the subtrees it removes are subtrees pruning
58/// would otherwise walk and work out column lists for.
59///
60/// Limit pushdown is second to last, which is to say it is immediately before top N. A limit that
61/// has moved below the projections above it is a limit that may now be sitting directly on a sort,
62/// and that pair is what top N fuses, so running the two the other way around would leave the fusion
63/// with a plan it cannot see the shape of.
64///
65/// Top N is last, because it is the one pass that fuses two operators into one rather than moving
66/// something around. Everything before it is written against a sort and a limit, and a pass that had
67/// to know about both spellings of the same plan is a pass with two of every rule in it.
68pub static PASSES: [&(dyn Pass + Sync); 7] = [
69    &fold::ExpressionRewriter,
70    &filter::FilterPushdown,
71    &empty::EmptyResultPullup,
72    &columns::UnusedColumns,
73    &limit::LimitPushdown,
74    &topn::TopN,
75    &late::LateMaterialization,
76];
77
78/// Rewrites a bound plan into the plan that runs, with every pass on.
79///
80/// # Errors
81///
82/// If a pass left the plan malformed or narrowed what it returns, which is a bug in the pass and
83/// not in the query.
84pub fn optimize(plan: &mut Plan) -> Result<()> {
85    optimize_with(plan, &Context::new())
86}
87
88/// Rewrites a bound plan into the plan that runs, skipping the passes the context turned off.
89///
90/// Every pass preserves the plan invariant, which is what [`Plan::validate`] checks, so this checks
91/// it once at the end rather than each pass checking itself. In a release build it does not, because
92/// a pass that breaks the invariant breaks it the same way in both builds and the debug build is
93/// where that gets found.
94///
95/// It also checks that the plan still returns as many columns as it did on the way in. A malformed
96/// plan is found by whatever runs next, but a rewrite that quietly changes what a query returns is
97/// the one failure that running the query afterwards would not notice, and column pruning in
98/// particular is a pass whose only way of being wrong is exactly that.
99///
100/// It also checks, in a debug build, that running the whole sequence a second time changes nothing.
101/// That is the property that makes a fixed sequence the right shape: a pass that keeps finding work
102/// on a plan it has already rewritten is a pass whose output depends on how many times it happened
103/// to run, and in a fixed sequence it runs once, so the plan that reaches the executor is whatever
104/// the first pass left behind. Each pass has its own test for this and the assertion is here anyway,
105/// because the pair that is not idempotent together is usually a pair that is idempotent apart.
106///
107/// # Errors
108///
109/// Whatever a pass reported, and then, in a debug build, if a pass left the plan malformed, narrowed
110/// what it returns or did not settle, all three of which are a bug in the pass and not in the query.
111pub fn optimize_with(plan: &mut Plan, context: &Context) -> Result<()> {
112    run(plan, context, &PASSES)
113}
114
115/// The sequence, over a list of passes the tests can choose.
116fn run(plan: &mut Plan, context: &Context, passes: &[&(dyn Pass + Sync)]) -> Result<()> {
117    let before = output_columns(plan, plan.root());
118    once(plan, context, passes)?;
119    if cfg!(debug_assertions) {
120        plan.validate()?;
121        let after = output_columns(plan, plan.root());
122        if after != before {
123            return Err(Error::internal(format!(
124                "a pass turned a query of {before} columns into one of {after}"
125            )));
126        }
127        let settled = plan.to_string();
128        once(plan, context, passes)?;
129        let again = plan.to_string();
130        if again != settled {
131            return Err(Error::internal(format!(
132                "the passes did not settle, since running them again gave a different plan\n\n{settled}\n{again}"
133            )));
134        }
135    }
136    Ok(())
137}
138
139/// One run of every pass that is turned on.
140fn once(plan: &mut Plan, context: &Context, passes: &[&(dyn Pass + Sync)]) -> Result<()> {
141    for pass in passes {
142        if context.is_disabled(pass.name()) {
143            continue;
144        }
145        pass.run(plan, context)?;
146    }
147    Ok(())
148}
149
150/// How many columns a node produces, which no pass is allowed to change at the root.
151///
152/// The count rather than the names and types, because the root of a plan the binder builds is a
153/// projection and what has to hold is that a pass did not add or drop one of its expressions. The
154/// recursion is over the operators that pass their input's width through, so its depth is the
155/// nesting the binder already walked to build the plan.
156fn output_columns(plan: &Plan, reference: NodeRef) -> usize {
157    match *plan.node(reference) {
158        Node::Get { columns, .. }
159        | Node::Values { columns, .. }
160        | Node::TableFunction { columns, .. }
161        | Node::Fetch { columns, .. } => plan.field_list(columns).len(),
162        Node::Project { exprs, .. } => plan.expr_list(exprs).len(),
163        Node::Aggregate { groups, aggregates, .. } => {
164            plan.expr_list(groups).len() + plan.expr_list(aggregates).len()
165        }
166        Node::Dummy => 0,
167        Node::Filter { input, .. }
168        | Node::Sort { input, .. }
169        | Node::Limit { input, .. }
170        | Node::TopN { input, .. }
171        | Node::Distinct { input, .. } => output_columns(plan, input),
172        // A set operation is as wide as either side, since the binder already required the two to
173        // agree. A join and a cross product are as wide as the two together.
174        Node::SetOp { left, .. } => output_columns(plan, left),
175        Node::Join { left, right, .. } | Node::CrossProduct { left, right } => {
176            output_columns(plan, left) + output_columns(plan, right)
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    /// How wide the plan a text prints is, before anything has run over it.
186    fn width(text: &str) -> usize {
187        let plan =
188            Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
189        output_columns(&plan, plan.root())
190    }
191
192    /// Optimize the plan a text prints and hand back what it printed afterwards.
193    fn optimized(text: &str) -> String {
194        let mut plan =
195            Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
196        optimize(&mut plan).unwrap_or_else(|error| panic!("{text} did not optimize: {error}"));
197        plan.to_string()
198    }
199
200    #[test]
201    fn the_width_of_a_plan_is_the_width_of_whatever_produces_its_columns() {
202        assert_eq!(
203            width(
204                "Project #1 [#0.0::INTEGER AS a]\n  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n"
205            ),
206            1
207        );
208        assert_eq!(width("Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n"), 2);
209        assert_eq!(width("Dummy\n"), 0);
210        assert_eq!(
211            width(
212                "Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]\n  Get memory.main.t AS t #0 [a::INTEGER]\n"
213            ),
214            2
215        );
216    }
217
218    /// A `LIMIT` or a `SORT` is as wide as what is under it, which is the recursion this function
219    /// exists for and the part a single level check would get wrong.
220    #[test]
221    fn an_operator_that_passes_its_input_through_is_as_wide_as_its_input() {
222        assert_eq!(
223            width("Limit 1 offset 0\n  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n"),
224            2
225        );
226    }
227
228    /// A join is both sides and a set operation is either one, since the binder already required
229    /// the two sides of a set operation to agree.
230    #[test]
231    fn a_join_is_both_sides_together_and_a_set_operation_is_one_of_them() {
232        assert_eq!(
233            width(
234                "Join INNER on=[]\n  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n  Get memory.main.u AS u #1 [x::INTEGER]\n"
235            ),
236            3
237        );
238        assert_eq!(
239            width(
240                "SetOp UNION ALL #2\n  Get memory.main.t AS t #0 [a::INTEGER]\n  Get memory.main.u AS u #1 [x::INTEGER]\n"
241            ),
242            1
243        );
244    }
245
246    /// The check is on the whole of `optimize` and not on one pass, so it keeps holding as passes
247    /// are added. This is the shape it runs over today.
248    #[test]
249    fn optimizing_keeps_a_query_as_wide_as_it_was() {
250        let before = "Project #1 [#0.1::VARCHAR AS b]\n  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
251        let after = "Project #1 [#0.0::VARCHAR AS b]\n  Get memory.main.t AS t #0 [b::VARCHAR]\n";
252        assert_eq!(optimized(before), after);
253        assert_eq!(width(before), width(after));
254    }
255
256    #[test]
257    fn no_two_passes_answer_to_the_same_name() {
258        // The name is the address, so two passes sharing one would make the toggle turn off
259        // whichever came first in the list and silently leave the other on.
260        let mut names: Vec<&str> = PASSES.iter().map(|pass| pass.name()).collect();
261        names.sort_unstable();
262        let held = names.len();
263        names.dedup();
264        assert_eq!(names.len(), held, "{names:?}");
265    }
266
267    #[test]
268    fn a_pass_that_is_turned_off_does_not_run() {
269        let text = "Project #1 [\"+\"(1::INTEGER, 1::INTEGER)::INTEGER AS n]\n  Get memory.main.t AS t #0 [a::INTEGER]\n";
270        let mut plan = Plan::parse(text).expect("a well formed plan");
271        let context = Context::without("expression_rewriter").expect("a name that is a pass");
272        optimize_with(&mut plan, &context).expect("the other pass still runs");
273        assert_eq!(
274            plan.to_string(),
275            "Project #1 [\"+\"(1::INTEGER, 1::INTEGER)::INTEGER AS n]\n  Get memory.main.t AS t #0 []\n"
276        );
277    }
278
279    /// A pass that finds the same work every time it looks, which is what the assertion is for.
280    #[derive(Debug)]
281    #[cfg(debug_assertions)]
282    struct Restless;
283
284    #[cfg(debug_assertions)]
285    impl Pass for Restless {
286        fn name(&self) -> &'static str {
287            "restless"
288        }
289
290        fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
291            let root = plan.root();
292            if !matches!(*plan.node(root), Node::Limit { .. }) {
293                return Ok(());
294            }
295            let stacked = plan.add_node(Node::Limit { input: root, count: Some(1), offset: 0 });
296            plan.set_root(stacked);
297            Ok(())
298        }
299    }
300
301    /// The settle check is a debug build check, so the test for it is a debug build test. Without
302    /// this the release profile job runs a test that asserts an error nothing was going to report,
303    /// which is what it had been doing since #196, because the per commit gate runs the tests once
304    /// and runs them in debug.
305    #[test]
306    #[cfg(debug_assertions)]
307    fn a_pass_that_never_settles_is_a_reported_error_and_not_a_plan() {
308        let text = "Limit 1 offset 0\n  Get memory.main.t AS t #0 [a::INTEGER]\n";
309        let mut plan = Plan::parse(text).expect("a well formed plan");
310        let error = run(&mut plan, &Context::new(), &[&Restless]).expect_err("it never settles");
311        assert!(error.message().starts_with("the passes did not settle"), "{}", error.message());
312    }
313
314    /// Folding before pruning, which is the reason the order in [`PASSES`] is the order it is. The
315    /// column is read only by a branch that cannot be taken, so one pass has to remove the branch
316    /// before the other can see that nothing reads the column.
317    #[test]
318    fn folding_runs_first_so_that_pruning_sees_the_columns_it_freed() {
319        let text = "Project #1 [CASE WHEN FALSE::BOOLEAN THEN #0.1::INTEGER ELSE #0.0::INTEGER END::INTEGER AS n]\n  Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n";
320        assert_eq!(
321            optimized(text),
322            "Project #1 [#0.0::INTEGER AS n]\n  Get memory.main.t AS t #0 [a::INTEGER]\n"
323        );
324    }
325}