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 dependent;
15pub mod distinct;
16pub mod empty;
17pub mod estimate;
18pub mod explain;
19pub mod filter;
20pub mod fold;
21pub mod late;
22pub mod limit;
23pub mod nulls;
24pub mod pass;
25pub mod tables;
26pub mod topn;
27mod transitive;
28mod walk;
29
30use rudb_common::{Error, Result};
31use rudb_plan::{Node, NodeRef, Plan};
32
33use crate::pass::{Context, Pass};
34
35/// The crate this rank belongs to, so that the layer check has something to read.
36pub const RANK: u8 = 11;
37
38/// The passes, in the order they run.
39///
40/// A fixed sequence rather than a loop to a fixed point, which is what `spec/09-optimizer.md`
41/// section 9.1 asks for and what DuckDB does. A fixed point is easy to write and hard to bound: a
42/// pair of passes that undo each other runs forever, and the version that stops after a few rounds
43/// has a plan that depends on how many rounds it was given.
44///
45/// Folding is before pruning because folding removes column references and pruning drops the columns
46/// nothing refers to, so a `CASE WHEN false THEN t.a ELSE 1 END` costs a column read when the two run
47/// the other way around. Nothing in the other direction is given up: pruning drops columns and
48/// renumbers bindings, and neither of those makes anything foldable.
49///
50/// Filter pushdown goes between them. After folding, because a predicate that folds to a constant is
51/// a predicate with nothing to push and the pass that moves it should not be the one that finds out.
52/// Before pruning, because moving a filter below a projection rewrites it in terms of columns the
53/// projection reads, and pruning has to see the plan after the move or it drops a column that
54/// something now refers to.
55///
56/// Empty result pullup is after filter pushdown, because pushdown is what moves an unsatisfiable
57/// predicate down to the scan it should stop and what drops the conjuncts that were always true, so
58/// the pass that looks for a predicate nothing can satisfy should look after that has happened. It
59/// is before pruning for the same reason folding is: the subtrees it removes are subtrees pruning
60/// would otherwise walk and work out column lists for.
61///
62/// Limit pushdown is second to last, which is to say it is immediately before top N. A limit that
63/// has moved below the projections above it is a limit that may now be sitting directly on a sort,
64/// and that pair is what top N fuses, so running the two the other way around would leave the fusion
65/// with a plan it cannot see the shape of.
66///
67/// The distinct aggregate rewrite is second, ahead of everything that moves an operator around,
68/// because it is the one pass that changes what an aggregate is rather than where it sits. Every
69/// other pass here is written against a single aggregate node, and running this one ahead of them
70/// means none of them has to know that `COUNT(DISTINCT x)` has a second spelling. In particular the
71/// limit that fuses into an aggregate has to fuse into the outer one, and after this pass the outer
72/// one is the only one it can see.
73///
74/// What it is not ahead of is folding, and that order is the other way round for a reason the AST
75/// fuzz target found. The rewrite fires only when every `DISTINCT` call in a node has the same
76/// argument, and whether two arguments are the same is a question folding answers: `max(DISTINCT
77/// 1 + 1)` and `min(DISTINCT 2)` are two arguments before it and one after it. With the rewrite
78/// first the pass sees the unfolded pair, refuses, and a second run of the sequence over its own
79/// output fires, which is the idempotence assertion below failing. Folding has no opinion about
80/// either spelling of an aggregate, so nothing is given up by putting it in front.
81///
82/// Top N is last, because it is the one pass that fuses two operators into one rather than moving
83/// something around. Everything before it is written against a sort and a limit, and a pass that had
84/// to know about both spellings of the same plan is a pass with two of every rule in it.
85pub static PASSES: [&(dyn Pass + Sync); 9] = [
86    &fold::ExpressionRewriter,
87    &distinct::DistinctAggregateRewrite,
88    &dependent::DependentGroupKeys,
89    &filter::FilterPushdown,
90    &empty::EmptyResultPullup,
91    &columns::UnusedColumns,
92    &limit::LimitPushdown,
93    &topn::TopN,
94    &late::LateMaterialization,
95];
96
97/// Every name `SET disabled_optimizers` accepts, which is every name DuckDB accepts.
98///
99/// `SELECT name FROM duckdb_optimizers()` on the pinned binary, sorted, all forty four of them.
100/// [`PASSES`] is the seven rudb has built and every name here is one rudb takes without complaint,
101/// because turning off a pass that does not exist is a thing that has already happened.
102///
103/// Accepting the other thirty seven is the whole point. Forty five files in the upstream corpus run
104/// a `SET disabled_optimizers`, and most of them name a pass rudb has not written, `join_order` and
105/// `build_side_probe_side` and `statistics_propagation` and the rest. Refusing those makes the
106/// `SET` fail, and a failed `SET` in a sqllogictest file ends the file, so every record after it
107/// goes unasked over a pass whose absence changes no answer.
108///
109/// The list is written down rather than discovered, because there is nothing to discover it from:
110/// DuckDB is a binary that may not be on the machine and this has to answer the same way when it is
111/// not. It is pinned to the same commit the rest of the compatibility work is pinned to, and a
112/// release that adds a pass adds a name here.
113pub static UPSTREAM: [&str; 44] = [
114    "aggregate_function_rewriter",
115    "aggregate_reuse",
116    "build_side_probe_side",
117    "column_lifetime",
118    "common_aggregate",
119    "common_subexpressions",
120    "common_subplan",
121    "compressed_materialization",
122    "cte_filter_pusher",
123    "cte_inlining",
124    "deliminator",
125    "distinct_aggregate_rewrite",
126    "duplicate_groups",
127    "empty_result_pullup",
128    "expression_rewriter",
129    "extension",
130    "filter_pullup",
131    "filter_pushdown",
132    "grouping_sets",
133    "in_clause",
134    "join_elimination",
135    "join_filter_pushdown",
136    "join_order",
137    "late_materialization",
138    "limit_pushdown",
139    "materialized_cte",
140    "outer_join_simplification",
141    "partial_aggregate_pushdown",
142    "partitioned_execution",
143    "projection_pullup",
144    "regex_range",
145    "remote_pushdown",
146    "reorder_filter",
147    "row_group_pruner",
148    "sampling_pushdown",
149    "scalar_fn_pushdown",
150    "statistics_propagation",
151    "top_n",
152    "top_n_window_elimination",
153    "type_pushdown",
154    "unnest_rewriter",
155    "unused_columns",
156    "window_rewriter",
157    "window_self_join",
158];
159
160/// Rewrites a bound plan into the plan that runs, with every pass on.
161///
162/// # Errors
163///
164/// If a pass left the plan malformed or narrowed what it returns, which is a bug in the pass and
165/// not in the query.
166pub fn optimize(plan: &mut Plan) -> Result<()> {
167    optimize_with(plan, &Context::new())
168}
169
170/// Rewrites a bound plan into the plan that runs, skipping the passes the context turned off.
171///
172/// Every pass preserves the plan invariant, which is what [`Plan::validate`] checks, so this checks
173/// it once at the end rather than each pass checking itself. In a release build it does not, because
174/// a pass that breaks the invariant breaks it the same way in both builds and the debug build is
175/// where that gets found.
176///
177/// It also checks that the plan still returns as many columns as it did on the way in. A malformed
178/// plan is found by whatever runs next, but a rewrite that quietly changes what a query returns is
179/// the one failure that running the query afterwards would not notice, and column pruning in
180/// particular is a pass whose only way of being wrong is exactly that.
181///
182/// It also checks, in a debug build, that running the whole sequence a second time changes nothing.
183/// That is the property that makes a fixed sequence the right shape: a pass that keeps finding work
184/// on a plan it has already rewritten is a pass whose output depends on how many times it happened
185/// to run, and in a fixed sequence it runs once, so the plan that reaches the executor is whatever
186/// the first pass left behind. Each pass has its own test for this and the assertion is here anyway,
187/// because the pair that is not idempotent together is usually a pair that is idempotent apart.
188///
189/// # Errors
190///
191/// Whatever a pass reported, and then, in a debug build, if a pass left the plan malformed, narrowed
192/// what it returns or did not settle, all three of which are a bug in the pass and not in the query.
193pub fn optimize_with(plan: &mut Plan, context: &Context) -> Result<()> {
194    run(plan, context, &PASSES)
195}
196
197/// The sequence, over a list of passes the tests can choose.
198fn run(plan: &mut Plan, context: &Context, passes: &[&(dyn Pass + Sync)]) -> Result<()> {
199    let before = output_columns(plan, plan.root());
200    once(plan, context, passes)?;
201    if cfg!(debug_assertions) {
202        plan.validate()?;
203        let after = output_columns(plan, plan.root());
204        if after != before {
205            return Err(Error::internal(format!(
206                "a pass turned a query of {before} columns into one of {after}"
207            )));
208        }
209        let settled = plan.to_string();
210        once(plan, context, passes)?;
211        let again = plan.to_string();
212        if again != settled {
213            return Err(Error::internal(format!(
214                "the passes did not settle, since running them again gave a different plan\n\n{settled}\n{again}"
215            )));
216        }
217    }
218    Ok(())
219}
220
221/// One run of every pass that is turned on.
222fn once(plan: &mut Plan, context: &Context, passes: &[&(dyn Pass + Sync)]) -> Result<()> {
223    for pass in passes {
224        if context.is_disabled(pass.name()) {
225            continue;
226        }
227        pass.run(plan, context)?;
228    }
229    Ok(())
230}
231
232/// How many columns a node produces, which no pass is allowed to change at the root.
233///
234/// The count rather than the names and types, because the root of a plan the binder builds is a
235/// projection and what has to hold is that a pass did not add or drop one of its expressions. The
236/// recursion is over the operators that pass their input's width through, so its depth is the
237/// nesting the binder already walked to build the plan.
238fn output_columns(plan: &Plan, reference: NodeRef) -> usize {
239    match *plan.node(reference) {
240        Node::Get { columns, .. }
241        | Node::Values { columns, .. }
242        | Node::TableFunction { columns, .. }
243        | Node::Fetch { columns, .. }
244        | Node::TableFetch { columns, .. } => plan.field_list(columns).len(),
245        Node::Project { exprs, .. } => plan.expr_list(exprs).len(),
246        Node::Aggregate { groups, aggregates, .. } => {
247            plan.expr_list(groups).len() + plan.expr_list(aggregates).len()
248        }
249        Node::Dummy => 0,
250        Node::Filter { input, .. }
251        | Node::Sort { input, .. }
252        | Node::Limit { input, .. }
253        | Node::TopN { input, .. }
254        | Node::Distinct { input, .. } => output_columns(plan, input),
255        // A set operation is as wide as either side, since the binder already required the two to
256        // agree. A join and a cross product are as wide as the two together.
257        Node::SetOp { left, .. } => output_columns(plan, left),
258        Node::Join { left, right, .. } | Node::CrossProduct { left, right } => {
259            output_columns(plan, left) + output_columns(plan, right)
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    /// How wide the plan a text prints is, before anything has run over it.
269    fn width(text: &str) -> usize {
270        let plan =
271            Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
272        output_columns(&plan, plan.root())
273    }
274
275    /// Optimize the plan a text prints and hand back what it printed afterwards.
276    fn optimized(text: &str) -> String {
277        let mut plan =
278            Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
279        optimize(&mut plan).unwrap_or_else(|error| panic!("{text} did not optimize: {error}"));
280        plan.to_string()
281    }
282
283    #[test]
284    fn the_width_of_a_plan_is_the_width_of_whatever_produces_its_columns() {
285        assert_eq!(
286            width(
287                "Project #1 [#0.0::INTEGER AS a]\n  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n"
288            ),
289            1
290        );
291        assert_eq!(width("Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n"), 2);
292        assert_eq!(width("Dummy\n"), 0);
293        assert_eq!(
294            width(
295                "Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]\n  Get memory.main.t AS t #0 [a::INTEGER]\n"
296            ),
297            2
298        );
299    }
300
301    /// A `LIMIT` or a `SORT` is as wide as what is under it, which is the recursion this function
302    /// exists for and the part a single level check would get wrong.
303    #[test]
304    fn an_operator_that_passes_its_input_through_is_as_wide_as_its_input() {
305        assert_eq!(
306            width("Limit 1 offset 0\n  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n"),
307            2
308        );
309    }
310
311    /// A join is both sides and a set operation is either one, since the binder already required
312    /// the two sides of a set operation to agree.
313    #[test]
314    fn a_join_is_both_sides_together_and_a_set_operation_is_one_of_them() {
315        assert_eq!(
316            width(
317                "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"
318            ),
319            3
320        );
321        assert_eq!(
322            width(
323                "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"
324            ),
325            1
326        );
327    }
328
329    /// The check is on the whole of `optimize` and not on one pass, so it keeps holding as passes
330    /// are added. This is the shape it runs over today.
331    #[test]
332    fn optimizing_keeps_a_query_as_wide_as_it_was() {
333        let before = "Project #1 [#0.1::VARCHAR AS b]\n  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
334        let after = "Project #1 [#0.0::VARCHAR AS b]\n  Get memory.main.t AS t #0 [b::VARCHAR]\n";
335        assert_eq!(optimized(before), after);
336        assert_eq!(width(before), width(after));
337    }
338
339    #[test]
340    fn no_two_passes_answer_to_the_same_name() {
341        // The name is the address, so two passes sharing one would make the toggle turn off
342        // whichever came first in the list and silently leave the other on.
343        let mut names: Vec<&str> = PASSES.iter().map(|pass| pass.name()).collect();
344        names.sort_unstable();
345        let held = names.len();
346        names.dedup();
347        assert_eq!(names.len(), held, "{names:?}");
348    }
349
350    #[test]
351    fn a_pass_that_is_turned_off_does_not_run() {
352        let text = "Project #1 [\"+\"(1::INTEGER, 1::INTEGER)::INTEGER AS n]\n  Get memory.main.t AS t #0 [a::INTEGER]\n";
353        let mut plan = Plan::parse(text).expect("a well formed plan");
354        let context = Context::without("expression_rewriter").expect("a name that is a pass");
355        optimize_with(&mut plan, &context).expect("the other pass still runs");
356        assert_eq!(
357            plan.to_string(),
358            "Project #1 [\"+\"(1::INTEGER, 1::INTEGER)::INTEGER AS n]\n  Get memory.main.t AS t #0 []\n"
359        );
360    }
361
362    /// A pass that finds the same work every time it looks, which is what the assertion is for.
363    #[derive(Debug)]
364    #[cfg(debug_assertions)]
365    struct Restless;
366
367    #[cfg(debug_assertions)]
368    impl Pass for Restless {
369        fn name(&self) -> &'static str {
370            "restless"
371        }
372
373        fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
374            let root = plan.root();
375            if !matches!(*plan.node(root), Node::Limit { .. }) {
376                return Ok(());
377            }
378            let stacked = plan.add_node(Node::Limit { input: root, count: Some(1), offset: 0 });
379            plan.set_root(stacked);
380            Ok(())
381        }
382    }
383
384    /// The settle check is a debug build check, so the test for it is a debug build test. Without
385    /// this the release profile job runs a test that asserts an error nothing was going to report,
386    /// which is what it had been doing since #196, because the per commit gate runs the tests once
387    /// and runs them in debug.
388    #[test]
389    #[cfg(debug_assertions)]
390    fn a_pass_that_never_settles_is_a_reported_error_and_not_a_plan() {
391        let text = "Limit 1 offset 0\n  Get memory.main.t AS t #0 [a::INTEGER]\n";
392        let mut plan = Plan::parse(text).expect("a well formed plan");
393        let error = run(&mut plan, &Context::new(), &[&Restless]).expect_err("it never settles");
394        assert!(error.message().starts_with("the passes did not settle"), "{}", error.message());
395    }
396
397    /// Folding before pruning, which is the reason the order in [`PASSES`] is the order it is. The
398    /// column is read only by a branch that cannot be taken, so one pass has to remove the branch
399    /// before the other can see that nothing reads the column.
400    #[test]
401    fn folding_runs_first_so_that_pruning_sees_the_columns_it_freed() {
402        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";
403        assert_eq!(
404            optimized(text),
405            "Project #1 [#0.0::INTEGER AS n]\n  Get memory.main.t AS t #0 [a::INTEGER]\n"
406        );
407    }
408
409    /// Folding before the distinct aggregate rewrite, which is the other half of that order. The
410    /// rewrite wants every `DISTINCT` call in a node to have the same argument, and these two have
411    /// the same argument only once folding has run, so with the passes the other way around the
412    /// rewrite refuses here and fires on a second run over its own output.
413    #[test]
414    fn folding_runs_first_so_that_the_distinct_rewrite_sees_one_argument_rather_than_two() {
415        let text = concat!(
416            "Aggregate #1 groups=[] aggregates=[max(DISTINCT \"+\"(1::INTEGER, 1::INTEGER)::INTEGER)::INTEGER, min(DISTINCT 2::INTEGER)::INTEGER]\n",
417            "  Dummy\n",
418        );
419        assert_eq!(
420            optimized(text),
421            concat!(
422                "Aggregate #1 groups=[] aggregates=[max(#2.0::INTEGER)::INTEGER, min(#2.0::INTEGER)::INTEGER]\n",
423                "  Aggregate #2 groups=[2::INTEGER] aggregates=[]\n",
424                "    Dummy\n",
425            )
426        );
427    }
428}