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 }
98 } else if accepts_a_compound_filter(block) {
99 push_into_compound(block, id, &conjuncts);
100 }
101 }
102}
103
104/// Copies each conjunct on the derived table into one `SELECT` that produces
105/// the derived table's rows, with the conjunct's columns replaced by the
106/// expressions that compute them in that `SELECT`.
107///
108/// @param arm - the `SELECT` (one arm of a compound, or the whole derived table)
109/// @param id - the derived table's statement-wide number
110/// @param conjuncts - the terms of the outer `WHERE`
111fn push_into_arm(arm: &mut BoundSelect, id: usize, conjuncts: &[BoundExpr]) {
112 for conjunct in conjuncts {
113 let mut used = Vec::new();
114 conjunct.sources_used(&mut used);
115 if used.as_slice() != [id] || !pushable(conjunct, id) {
116 continue;
117 }
118 let Some(inner) = substituted(conjunct, id, arm) else {
119 continue;
120 };
121 arm.filter = Some(match arm.filter.take() {
122 Some(existing) => BoundExpr::And(Box::new(existing), Box::new(inner)),
123 None => inner,
124 });
125 }
126}
127
128/// Copies the conjuncts into every arm of a compound derived table.
129///
130/// **Each arm compares with its own columns' affinity and collation.** SQLite
131/// pushes the term into every arm, and a term `x > 7` over `SELECT x FROM t
132/// UNION ALL SELECT y FROM v` with a TEXT `x` and an INTEGER `y` compares text
133/// with text in the first arm and integers in the second. Tested after the
134/// compound, the column has no affinity and a text `'1'` is greater than the
135/// integer 7 by storage class. An arm that cannot take a filter (a `VALUES`
136/// list, an aggregate arm) is left as it is; the outer term stays in place.
137///
138/// @param block - the compound derived table; its first arm is the block itself
139/// @param id - the derived table's statement-wide number
140/// @param conjuncts - the terms of the outer `WHERE`
141fn push_into_compound(block: &mut BoundSelect, id: usize, conjuncts: &[BoundExpr]) {
142 if accepts_an_arm_filter(block) {
143 push_into_arm(block, id, conjuncts);
144 }
145 for (_, arm) in &mut block.compounds {
146 if accepts_an_arm_filter(arm) && arm.compounds.is_empty() {
147 push_into_arm(arm, id, conjuncts);
148 }
149 }
150}
151
152/// Reports whether a filter on a compound's result may be copied into its arms.
153///
154/// SQLite refuses a compound with a `LIMIT` or `OFFSET`, which count rows
155/// before the filter, and a compound that has a window function in any arm.
156/// When an arm is joined by `UNION`, `INTERSECT` or `EXCEPT` it also refuses
157/// when the compound's `ORDER BY` has a term that is not a result column.
158///
159/// @param block - the compound derived table
160fn accepts_a_compound_filter(block: &BoundSelect) -> bool {
161 let all_union_all = block
162 .compounds
163 .iter()
164 .all(|(op, _)| *op == crate::ast::CompoundOp::UnionAll);
165 block.limit.is_none()
166 && block.offset.is_none()
167 && (all_union_all || block.order_by.is_empty())
168 && block.windows.is_empty()
169 && block
170 .compounds
171 .iter()
172 .all(|(_, arm)| arm.windows.is_empty())
173}
174
175/// Reports whether one arm of a compound can take a copied filter.
176///
177/// An arm with its own `DISTINCT`, grouping, aggregate or window computes its
178/// result columns over rows a filter would remove, and a `VALUES` list has no
179/// expressions to substitute.
180///
181/// @param arm - one arm of the compound
182fn accepts_an_arm_filter(arm: &BoundSelect) -> bool {
183 !arm.distinct
184 && arm.group_by.is_empty()
185 && arm.aggregates.is_empty()
186 && arm.having.is_none()
187 && arm.windows.is_empty()
188 && arm.values.is_empty()
189}
190
191/// Reports whether a derived table's rows are the same whether a condition on
192/// its result columns is applied before it builds them or after.
193///
194/// **Not for a `LIMIT` or an `OFFSET`**, which count rows before the condition;
195/// **not for `DISTINCT`**, which keeps one row of several equal under its own
196/// collation, so a condition under a different collation can keep a different
197/// one; **not for grouping or a window**, whose result columns are computed
198/// over rows the condition would remove; and **not for a compound**, which is
199/// several blocks.
200///
201/// @param block - the derived table's query
202fn accepts_a_pushed_filter(block: &BoundSelect) -> bool {
203 block.compounds.is_empty()
204 && block.limit.is_none()
205 && block.offset.is_none()
206 && !block.distinct
207 && block.group_by.is_empty()
208 && block.aggregates.is_empty()
209 && block.having.is_none()
210 && block.windows.is_empty()
211 && block.values.is_empty()
212}
213
214/// Reports whether a condition is one that may be evaluated anywhere, any
215/// number of times, with the same answer.
216///
217/// A whitelist: a subquery, an aggregate, a window value, a registered function
218/// whose determinism the planner cannot see, and the scalar functions whose
219/// answer changes from call to call are all refused.
220///
221/// @param expr - the condition, or a part of it
222/// @param id - the derived table's statement-wide number; its rowid has no
223/// inner expression to stand for it
224fn pushable(expr: &BoundExpr, id: usize) -> bool {
225 let this = match expr {
226 BoundExpr::Rowid { source } => *source != id,
227 BoundExpr::Subquery { .. }
228 | BoundExpr::Aggregate { .. }
229 | BoundExpr::WindowRef { .. }
230 | BoundExpr::SorterColumn { .. }
231 | BoundExpr::External { .. }
232 | BoundExpr::VirtualFunction { .. }
233 | BoundExpr::Raise { .. } => false,
234 BoundExpr::Function { func, .. } => !matches!(
235 func,
236 crate::function::ScalarFunc::Random
237 | crate::function::ScalarFunc::RandomBlob
238 | crate::function::ScalarFunc::Changes
239 | crate::function::ScalarFunc::TotalChanges
240 | crate::function::ScalarFunc::LastInsertRowid
241 ),
242 _ => true,
243 };
244 this && expr.children().iter().all(|child| pushable(child, id))
245}
246
247/// Reports whether a condition may be tested more than once with the same
248/// answer each time.
249///
250/// A condition on an outer term can be tested before a lateral join runs its
251/// function, so that the function is not called for rows the condition
252/// removes. It is tested again with the rest of the `WHERE`, which is only
253/// harmless for a condition with no subquery, no random function and no
254/// registered function whose determinism the planner cannot see.
255///
256/// @param expr - the condition
257pub fn is_repeatable_condition(expr: &BoundExpr) -> bool {
258 pushable(expr, usize::MAX)
259}
260
261/// Reports whether an expression calls a function whose answer changes from one
262/// call to the next, such as `random()`.
263///
264/// @param expr - the expression, or a part of it
265pub fn calls_a_volatile_function(expr: &BoundExpr) -> bool {
266 let this = matches!(
267 expr,
268 BoundExpr::Function {
269 func: crate::function::ScalarFunc::Random
270 | crate::function::ScalarFunc::RandomBlob
271 | crate::function::ScalarFunc::Changes
272 | crate::function::ScalarFunc::TotalChanges
273 | crate::function::ScalarFunc::LastInsertRowid,
274 ..
275 }
276 );
277 this || expr
278 .children()
279 .iter()
280 .any(|child| calls_a_volatile_function(child))
281}
282
283/// Returns a condition with each of the derived table's columns replaced by
284/// the expression that computes it inside the derived table.
285///
286/// `None` when a column's expression is one that should not be evaluated in a
287/// `WHERE`, such as a correlated subquery: the condition is then left outside,
288/// where it was.
289///
290/// @param conjunct - the condition, over the derived table's columns
291/// @param id - the derived table's statement-wide number
292/// @param block - the derived table's query
293fn substituted(conjunct: &BoundExpr, id: usize, block: &BoundSelect) -> Option<BoundExpr> {
294 let mut copy = conjunct.clone();
295 replace_columns(&mut copy, id, block).then_some(copy)
296}
297
298/// Replaces the derived table's columns in place, reporting whether every one
299/// could be replaced.
300///
301/// @param expr - the expression being rewritten
302/// @param id - the derived table's statement-wide number
303/// @param block - the derived table's query
304fn replace_columns(expr: &mut BoundExpr, id: usize, block: &BoundSelect) -> bool {
305 if let BoundExpr::Column { source, column, .. } = expr {
306 if *source != id {
307 return true;
308 }
309 let Some(inner) = block.columns.get(usize::from(*column)) else {
310 return false;
311 };
312 if !pushable(&inner.expr, usize::MAX) {
313 return false;
314 }
315 *expr = inner.expr.clone();
316 return true;
317 }
318 let replaced = expr
319 .children_mut()
320 .into_iter()
321 .all(|child| replace_columns(child, id, block));
322 if replaced {
323 refresh_comparison_rules(expr);
324 }
325 replaced
326}
327
328/// Recomputes the affinity and collation a comparison applies, from its
329/// operands as they are now.
330///
331/// A comparison fixes both when the statement is bound, from the operands it
332/// was written with. After a derived table's column is replaced by the
333/// expression of one arm, the operand may have a different affinity: the
334/// derived table's column of a compound has none when the arms disagree, and
335/// the arm's own column has its declared one. SQLite compares the substituted
336/// expression, so the comparison is rebuilt from it.
337///
338/// @param expr - the expression whose children were just substituted
339fn refresh_comparison_rules(expr: &mut BoundExpr) {
340 use crate::bind::comparison_rules;
341 match expr {
342 BoundExpr::Compare {
343 left,
344 right,
345 affinity,
346 collation,
347 ..
348 }
349 | BoundExpr::Is {
350 left,
351 right,
352 affinity,
353 collation,
354 ..
355 } => (*affinity, *collation) = comparison_rules(left, right),
356 BoundExpr::Between {
357 operand,
358 low,
359 high,
360 low_affinity,
361 low_collation,
362 high_affinity,
363 high_collation,
364 ..
365 } => {
366 (*low_affinity, *low_collation) = comparison_rules(operand, low);
367 (*high_affinity, *high_collation) = comparison_rules(operand, high);
368 }
369 BoundExpr::InList {
370 operand,
371 list,
372 affinity,
373 collation,
374 ..
375 } => {
376 if let Some(first) = list.first() {
377 (*affinity, *collation) = comparison_rules(operand, first);
378 }
379 }
380 BoundExpr::Case {
381 operand: Some(operand),
382 branches,
383 comparisons,
384 ..
385 } => {
386 *comparisons = branches
387 .iter()
388 .map(|(when, _)| comparison_rules(operand, when))
389 .collect();
390 }
391 _ => {}
392 }
393}