Skip to main content

inillucent_sql/plan/
pushdown.rs

1//! Pushing a `WHERE` condition on a derived table's columns into the derived
2//! table.
3//!
4//! Invariant: **a condition is copied into a derived table only when doing so
5//! cannot change which rows the statement returns, and the original stays where
6//! it was.** The copy is a filter the inner query applies before it builds its
7//! rows, so the inner planner can seek by key instead of building every row of
8//! a view. The outer condition is still tested on every row the derived table
9//! produces, so if a rule here were too generous the cost would be a condition
10//! tested twice, never a row that should have been filtered.
11//!
12//! This is SQLite's push-down optimisation, restricted to the cases where it is
13//! plainly sound. Measured on the coffee shop example's database before it:
14//! `SELECT * FROM order_summary WHERE id = 57` over a view of three joins and a
15//! correlated subquery took 2.50 ms against 0.048 ms for the same query written
16//! without the view, because every row of the view, correlated subquery
17//! included, was built before the `WHERE` was applied. SQLite plans both as a
18//! primary key search.
19//!
20//! Here rather than in [`super`] because `plan.rs` is at its recorded size.
21
22use super::*;
23
24mod unused;
25
26/// Copies each `WHERE` conjunct that reads only one derived table's columns
27/// into that derived table's own `WHERE`, and then replaces each derived
28/// table's unread result columns with NULL.
29///
30/// The second step is in [`unused`]. It runs here because it is the other
31/// half of treating a derived table the way SQLite's flattener does, and
32/// `plan.rs` is at its recorded size.
33///
34/// @param select - the statement being planned, whose derived tables may gain
35///   a filter and lose result columns
36pub(super) fn push_into_derived_tables(select: &mut BoundSelect) {
37    push_filters(select);
38    unused::drop_unread_columns(select);
39}
40
41/// Copies each `WHERE` conjunct that reads only one derived table's columns
42/// into that derived table's own `WHERE`.
43///
44/// @param select - the statement being planned, whose derived tables may gain
45///   a filter
46fn push_filters(select: &mut BoundSelect) {
47    let Some(filter) = select.filter.as_ref() else {
48        return;
49    };
50    // Nothing is split or copied for a statement with no derived table, which
51    // is almost every statement: the compile of `SELECT id FROM t WHERE email
52    // = ?1` has an allocation budget, and splitting its `WHERE` here took three
53    // of them to find nothing to push.
54    if !select
55        .sources
56        .iter()
57        .any(|source| matches!(source.rows, SourceRows::Subquery(_)))
58    {
59        return;
60    }
61    // A `RIGHT` or `FULL` join can null extend any term before it, so a
62    // statement with one pushes nothing.
63    if select
64        .sources
65        .iter()
66        .any(|source| matches!(source.join, JoinKind::Right | JoinKind::Full))
67    {
68        return;
69    }
70    let conjuncts = conjunction(filter);
71    for source in &mut select.sources {
72        // The right side of a `LEFT JOIN` is null extended when nothing in it
73        // matches, and a condition such as `v.x IS NULL` is true of a null
74        // extended row and false of the rows the push would have removed.
75        if source.join == JoinKind::Left {
76            continue;
77        }
78        let id = source.id;
79        let SourceRows::Subquery(block) = &mut source.rows else {
80            continue;
81        };
82        // The references of a shared common table expression read one set of
83        // rows, so a filter pushed into one of them would change the others.
84        if block.shared.is_some() {
85            continue;
86        }
87        // SQLite pushes nothing into a `MATERIALIZED` CTE, nor into a CTE named
88        // by more than one FROM term, which it fills once for all of them.
89        if source.derived.cte
90            && (source.derived.materialized == Some(true) || source.derived.uses >= 2)
91        {
92            continue;
93        }
94        if block.compounds.is_empty() {
95            if accepts_a_pushed_filter(block) {
96                push_into_arm(block, id, &conjuncts);
97            } else if accepts_a_grouped_filter(block) {
98                let on_groups: Vec<BoundExpr> = conjuncts
99                    .iter()
100                    .filter(|conjunct| reads_only_grouping_columns(conjunct, id, block))
101                    .cloned()
102                    .collect();
103                push_into_arm(block, id, &on_groups);
104            }
105        } else if accepts_a_compound_filter(block) {
106            push_into_compound(block, id, &conjuncts);
107        }
108    }
109}
110
111/// Copies each conjunct on the derived table into one `SELECT` that produces
112/// the derived table's rows, with the conjunct's columns replaced by the
113/// expressions that compute them in that `SELECT`.
114///
115/// @param arm - the `SELECT` (one arm of a compound, or the whole derived table)
116/// @param id - the derived table's statement-wide number
117/// @param conjuncts - the terms of the outer `WHERE`
118fn push_into_arm(arm: &mut BoundSelect, id: usize, conjuncts: &[BoundExpr]) {
119    for conjunct in conjuncts {
120        let mut used = Vec::new();
121        conjunct.sources_used(&mut used);
122        if used.as_slice() != [id] || !pushable(conjunct, id) {
123            continue;
124        }
125        let Some(inner) = substituted(conjunct, id, arm) else {
126            continue;
127        };
128        arm.filter = Some(match arm.filter.take() {
129            Some(existing) => BoundExpr::And(Box::new(existing), Box::new(inner)),
130            None => inner,
131        });
132    }
133}
134
135/// Copies the conjuncts into every arm of a compound derived table.
136///
137/// **Each arm compares with its own columns' affinity and collation.** SQLite
138/// pushes the term into every arm, and a term `x > 7` over `SELECT x FROM t
139/// UNION ALL SELECT y FROM v` with a TEXT `x` and an INTEGER `y` compares text
140/// with text in the first arm and integers in the second. Tested after the
141/// compound, the column has no affinity and a text `'1'` is greater than the
142/// integer 7 by storage class. An arm that cannot take a filter (a `VALUES`
143/// list, an aggregate arm) is left as it is; the outer term stays in place.
144///
145/// @param block - the compound derived table; its first arm is the block itself
146/// @param id - the derived table's statement-wide number
147/// @param conjuncts - the terms of the outer `WHERE`
148fn push_into_compound(block: &mut BoundSelect, id: usize, conjuncts: &[BoundExpr]) {
149    if accepts_an_arm_filter(block) {
150        push_into_arm(block, id, conjuncts);
151    }
152    for (_, arm) in &mut block.compounds {
153        if accepts_an_arm_filter(arm) && arm.compounds.is_empty() {
154            push_into_arm(arm, id, conjuncts);
155        }
156    }
157}
158
159/// Reports whether a filter on a compound's result may be copied into its arms.
160///
161/// SQLite refuses a compound with a `LIMIT` or `OFFSET`, which count rows
162/// before the filter, and a compound that has a window function in any arm.
163/// When an arm is joined by `UNION`, `INTERSECT` or `EXCEPT` it also refuses
164/// when the compound's `ORDER BY` has a term that is not a result column.
165///
166/// @param block - the compound derived table
167fn accepts_a_compound_filter(block: &BoundSelect) -> bool {
168    let all_union_all = block
169        .compounds
170        .iter()
171        .all(|(op, _)| *op == crate::ast::CompoundOp::UnionAll);
172    // SQLite refuses to push into a compound joined by anything but UNION ALL
173    // when any result column of any arm has a collation other than BINARY. The
174    // compound removes duplicates under that collation and the pushed term
175    // compares under its own, so the two can keep different rows:
176    // `SELECT * FROM (SELECT a FROM t1 INTERSECT SELECT b FROM t2) WHERE a||''
177    // = 'ABC'` with NOCASE columns keeps 'ABC' only when nothing is pushed.
178    let binary_columns = |arm: &BoundSelect| {
179        arm.columns
180            .iter()
181            .all(|column| crate::bind::result_collation(&column.expr) == Collation::Binary)
182    };
183    block.limit.is_none()
184        && block.offset.is_none()
185        && (all_union_all || block.order_by.is_empty())
186        && (all_union_all
187            || (binary_columns(block)
188                && block.compounds.iter().all(|(_, arm)| binary_columns(arm))))
189        && block.windows.is_empty()
190        && block
191            .compounds
192            .iter()
193            .all(|(_, arm)| arm.windows.is_empty())
194}
195
196/// Reports whether one arm of a compound can take a copied filter.
197///
198/// An arm with its own `DISTINCT`, grouping, aggregate or window computes its
199/// result columns over rows a filter would remove, and a `VALUES` list has no
200/// expressions to substitute.
201///
202/// @param arm - one arm of the compound
203fn accepts_an_arm_filter(arm: &BoundSelect) -> bool {
204    !arm.distinct
205        && arm.group_by.is_empty()
206        && arm.aggregates.is_empty()
207        && arm.having.is_none()
208        && arm.windows.is_empty()
209        && arm.values.is_empty()
210}
211
212/// Reports whether a derived table's rows are the same whether a condition on
213/// its result columns is applied before it builds them or after.
214///
215/// **Not for a `LIMIT` or an `OFFSET`**, which count rows before the condition;
216/// **not for `DISTINCT`**, which keeps one row of several equal under its own
217/// collation, so a condition under a different collation can keep a different
218/// one; **not for grouping or a window**, whose result columns are computed
219/// over rows the condition would remove; and **not for a compound**, which is
220/// several blocks.
221///
222/// @param block - the derived table's query
223fn accepts_a_pushed_filter(block: &BoundSelect) -> bool {
224    block.compounds.is_empty()
225        && block.limit.is_none()
226        && block.offset.is_none()
227        && !block.distinct
228        && block.group_by.is_empty()
229        && block.aggregates.is_empty()
230        && block.having.is_none()
231        && block.windows.is_empty()
232        && block.values.is_empty()
233}
234
235/// Reports whether a grouped derived table can take a filter on its grouping
236/// columns.
237///
238/// **SQLite's push down into an aggregate.** A condition that reads only the
239/// columns a derived table groups by keeps or removes whole groups, so it can
240/// be tested on the rows before they are grouped. That is what lets `SELECT s
241/// FROM (SELECT g, sum(v) s FROM t GROUP BY g) WHERE g = 5` search an index on
242/// `g` rather than group the whole table: a view that sums per customer,
243/// read for one customer, cost the whole table per read, and a correlated
244/// lookup into such a view 3,000 times took nine times SQLite's time. Not with
245/// a window, a `DISTINCT` or a `LIMIT`, which work on the grouped rows.
246///
247/// @param block - the derived table's query
248fn accepts_a_grouped_filter(block: &BoundSelect) -> bool {
249    block.compounds.is_empty()
250        && !block.group_by.is_empty()
251        && block.limit.is_none()
252        && block.offset.is_none()
253        && !block.distinct
254        && block.windows.is_empty()
255        && block.values.is_empty()
256}
257
258/// Reports whether every derived table column a condition reads is computed
259/// by an expression the derived table groups by.
260///
261/// @param conjunct - the condition, over the derived table's columns
262/// @param id - the derived table's statement-wide number
263/// @param block - the derived table's query
264fn reads_only_grouping_columns(conjunct: &BoundExpr, id: usize, block: &BoundSelect) -> bool {
265    let mut grouped = true;
266    let mut probe = conjunct.clone();
267    crate::rewrite::rewrite_expr(&mut probe, &mut |expr: &mut BoundExpr| {
268        if let BoundExpr::Column { source, column, .. } = expr {
269            if *source == id {
270                let computed = block
271                    .columns
272                    .get(usize::from(*column))
273                    .map(|held| &held.expr);
274                grouped &=
275                    computed.is_some_and(|inner| block.group_by.iter().any(|key| key == inner));
276            }
277        }
278    });
279    grouped
280}
281
282/// Reports whether a condition is one that may be evaluated anywhere, any
283/// number of times, with the same answer.
284///
285/// A whitelist: a subquery, an aggregate, a window value, a registered function
286/// whose determinism the planner cannot see, and the scalar functions whose
287/// answer changes from call to call are all refused.
288///
289/// @param expr - the condition, or a part of it
290/// @param id - the derived table's statement-wide number; its rowid has no
291///   inner expression to stand for it
292fn pushable(expr: &BoundExpr, id: usize) -> bool {
293    let this = match expr {
294        BoundExpr::Rowid { source } => *source != id,
295        BoundExpr::Subquery { .. }
296        | BoundExpr::Aggregate { .. }
297        | BoundExpr::WindowRef { .. }
298        | BoundExpr::SorterColumn { .. }
299        | BoundExpr::External { .. }
300        | BoundExpr::VirtualFunction { .. }
301        | BoundExpr::Raise { .. } => false,
302        BoundExpr::Function { func, .. } => !matches!(
303            func,
304            crate::function::ScalarFunc::Random
305                | crate::function::ScalarFunc::RandomBlob
306                | crate::function::ScalarFunc::Changes
307                | crate::function::ScalarFunc::TotalChanges
308                | crate::function::ScalarFunc::LastInsertRowid
309        ),
310        _ => true,
311    };
312    this && expr.children().iter().all(|child| pushable(child, id))
313}
314
315/// Reports whether a condition may be tested more than once with the same
316/// answer each time.
317///
318/// A condition on an outer term can be tested before a lateral join runs its
319/// function, so that the function is not called for rows the condition
320/// removes. It is tested again with the rest of the `WHERE`, which is only
321/// harmless for a condition with no subquery, no random function and no
322/// registered function whose determinism the planner cannot see.
323///
324/// @param expr - the condition
325pub fn is_repeatable_condition(expr: &BoundExpr) -> bool {
326    pushable(expr, usize::MAX)
327}
328
329/// Reports whether an expression calls a function whose answer changes from one
330/// call to the next, such as `random()`.
331///
332/// @param expr - the expression, or a part of it
333pub fn calls_a_volatile_function(expr: &BoundExpr) -> bool {
334    let this = matches!(
335        expr,
336        BoundExpr::Function {
337            func: crate::function::ScalarFunc::Random
338                | crate::function::ScalarFunc::RandomBlob
339                | crate::function::ScalarFunc::Changes
340                | crate::function::ScalarFunc::TotalChanges
341                | crate::function::ScalarFunc::LastInsertRowid,
342            ..
343        }
344    );
345    this || expr
346        .children()
347        .iter()
348        .any(|child| calls_a_volatile_function(child))
349}
350
351/// Returns a condition with each of the derived table's columns replaced by
352/// the expression that computes it inside the derived table.
353///
354/// `None` when a column's expression is one that should not be evaluated in a
355/// `WHERE`, such as a correlated subquery: the condition is then left outside,
356/// where it was.
357///
358/// @param conjunct - the condition, over the derived table's columns
359/// @param id - the derived table's statement-wide number
360/// @param block - the derived table's query
361fn substituted(conjunct: &BoundExpr, id: usize, block: &BoundSelect) -> Option<BoundExpr> {
362    let mut copy = conjunct.clone();
363    replace_columns(&mut copy, id, block).then_some(copy)
364}
365
366/// Replaces the derived table's columns in place, reporting whether every one
367/// could be replaced.
368///
369/// @param expr - the expression being rewritten
370/// @param id - the derived table's statement-wide number
371/// @param block - the derived table's query
372fn replace_columns(expr: &mut BoundExpr, id: usize, block: &BoundSelect) -> bool {
373    if let BoundExpr::Column {
374        source,
375        column,
376        collation: outer_collation,
377        ..
378    } = expr
379    {
380        if *source != id {
381            return true;
382        }
383        let outer_collation = *outer_collation;
384        let Some(inner) = block.columns.get(usize::from(*column)) else {
385            return false;
386        };
387        if !pushable(&inner.expr, usize::MAX) {
388            return false;
389        }
390        // A JSON call stands behind a unary plus, so the pushed copy reads the
391        // column as the plain text the derived table hands out; see `inline`
392        // in `flatten.rs`. Pushed bare, `WHERE NOT json_quote(c0)` over
393        // `SELECT json(TRUE) AS c0` read the JSON mark and kept no row.
394        *expr = match &inner.expr {
395            BoundExpr::Json { .. } => BoundExpr::Unary {
396                op: crate::ast::UnaryOp::Identity,
397                operand: Box::new(inner.expr.clone()),
398            },
399            other => other.clone(),
400        };
401        *expr = with_derived_collation(std::mem::replace(expr, BoundExpr::Null), outer_collation);
402        return true;
403    }
404    let replaced = expr
405        .children_mut()
406        .into_iter()
407        .all(|child| replace_columns(child, id, block));
408    if replaced {
409        refresh_comparison_rules(expr);
410    }
411    replaced
412}
413
414/// Makes a substituted expression compare with the collation the derived
415/// table's column had.
416///
417/// SQLite's `substExpr` wraps the replacement in a `COLLATE` when its own
418/// collation differs from the one the replaced column had. For a compound
419/// derived table the column has the leftmost arm's collation, so a filter
420/// `b = 'BbB'` over `SELECT a, b FROM t1 UNION ALL SELECT c, d FROM t2` with a
421/// NOCASE `t1.b` must compare `t2.d` with NOCASE as well. Without the wrapper
422/// each arm compared with its own column's collation and the BINARY arm
423/// missed the row the unpushed filter would have kept.
424///
425/// @param replacement - the expression that replaced the derived column
426/// @param outer - the collation the derived table's column had
427fn with_derived_collation(replacement: BoundExpr, outer: Collation) -> BoundExpr {
428    if crate::bind::result_collation(&replacement) == outer {
429        return replacement;
430    }
431    BoundExpr::Collate {
432        operand: Box::new(replacement),
433        collation: outer,
434    }
435}
436
437/// Recomputes the affinity and collation a comparison applies, from its
438/// operands as they are now.
439///
440/// A comparison fixes both when the statement is bound, from the operands it
441/// was written with. After a derived table's column is replaced by the
442/// expression of one arm, the operand may have a different affinity: the
443/// derived table's column of a compound has none when the arms disagree, and
444/// the arm's own column has its declared one. SQLite compares the substituted
445/// expression, so the comparison is rebuilt from it.
446///
447/// @param expr - the expression whose children were just substituted
448fn refresh_comparison_rules(expr: &mut BoundExpr) {
449    use crate::bind::comparison_rules;
450    match expr {
451        BoundExpr::Compare {
452            left,
453            right,
454            affinity,
455            collation,
456            ..
457        }
458        | BoundExpr::Is {
459            left,
460            right,
461            affinity,
462            collation,
463            ..
464        } => (*affinity, *collation) = comparison_rules(left, right),
465        BoundExpr::Between {
466            operand,
467            low,
468            high,
469            low_affinity,
470            low_collation,
471            high_affinity,
472            high_collation,
473            ..
474        } => {
475            (*low_affinity, *low_collation) = comparison_rules(operand, low);
476            (*high_affinity, *high_collation) = comparison_rules(operand, high);
477        }
478        BoundExpr::InList {
479            operand,
480            list,
481            affinity,
482            collation,
483            ..
484        } => {
485            if let Some(first) = list.first() {
486                (*affinity, *collation) = comparison_rules(operand, first);
487            }
488        }
489        BoundExpr::Case {
490            operand: Some(operand),
491            branches,
492            comparisons,
493            ..
494        } => {
495            *comparisons = branches
496                .iter()
497                .map(|(when, _)| comparison_rules(operand, when))
498                .collect();
499        }
500        _ => {}
501    }
502}