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}