mz_sql/plan/transform_hir.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Transformations of SQL IR, before decorrelation.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::sync::LazyLock;
14use std::{iter, mem};
15
16use itertools::Itertools;
17use mz_expr::WindowFrame;
18use mz_expr::func::variadic::RecordCreate;
19use mz_expr::visit::{Visit, VisitChildren};
20use mz_expr::{ColumnOrder, UnaryFunc, VariadicFunc};
21use mz_ore::stack::RecursionLimitError;
22use mz_repr::{ColumnName, SqlColumnType, SqlRelationType, SqlScalarType};
23
24use crate::plan::hir::{
25 AbstractExpr, AggregateFunc, AggregateWindowExpr, ColumnRef, HirRelationExpr, HirScalarExpr,
26 ValueWindowExpr, ValueWindowFunc, WindowExpr,
27};
28use crate::plan::{AggregateExpr, WindowExprType};
29
30/// Rewrites predicates that contain subqueries so that the subqueries
31/// appear in their own later predicate when possible.
32///
33/// For example, this function rewrites this expression
34///
35/// ```text
36/// Filter {
37/// predicates: [a = b AND EXISTS (<subquery 1>) AND c = d AND (<subquery 2>) = e]
38/// }
39/// ```
40///
41/// like so:
42///
43/// ```text
44/// Filter {
45/// predicates: [
46/// a = b AND c = d,
47/// EXISTS (<subquery>),
48/// (<subquery 2>) = e,
49/// ]
50/// }
51/// ```
52///
53/// The rewrite causes decorrelation to incorporate prior predicates into
54/// the outer relation upon which the subquery is evaluated. In the above
55/// rewritten example, the `EXISTS (<subquery>)` will only be evaluated for
56/// outer rows where `a = b AND c = d`. The second subquery, `(<subquery 2>)
57/// = e`, will be further restricted to outer rows that match `A = b AND c =
58/// d AND EXISTS(<subquery>)`. This can vastly reduce the cost of the
59/// subquery, especially when the original conjunction contains join keys.
60pub fn split_subquery_predicates(expr: &mut HirRelationExpr) -> Result<(), RecursionLimitError> {
61 fn walk_relation(expr: &mut HirRelationExpr) -> Result<(), RecursionLimitError> {
62 #[allow(deprecated)]
63 expr.visit_mut_fallible(0, &mut |expr, _| {
64 match expr {
65 HirRelationExpr::Map { scalars, .. } => {
66 for scalar in scalars {
67 walk_scalar(scalar)?;
68 }
69 }
70 HirRelationExpr::CallTable { exprs, .. } => {
71 for expr in exprs {
72 walk_scalar(expr)?;
73 }
74 }
75 HirRelationExpr::Filter { predicates, .. } => {
76 let mut subqueries = vec![];
77 for predicate in &mut *predicates {
78 walk_scalar(predicate)?;
79 extract_conjuncted_subqueries(predicate, &mut subqueries)?;
80 }
81 // TODO(benesch): we could be smarter about the order in which
82 // we emit subqueries. At the moment we just emit in the order
83 // we discovered them, but ideally we'd emit them in an order
84 // that accounted for their cost/selectivity. E.g., low-cost,
85 // high-selectivity subqueries should go first.
86 for subquery in subqueries {
87 predicates.push(subquery);
88 }
89 }
90 _ => (),
91 }
92 Ok(())
93 })
94 }
95
96 fn walk_scalar(expr: &mut HirScalarExpr) -> Result<(), RecursionLimitError> {
97 expr.try_visit_direct_subqueries_mut(&mut walk_relation)
98 }
99
100 fn contains_subquery(expr: &HirScalarExpr) -> Result<bool, RecursionLimitError> {
101 let mut found = false;
102 expr.try_visit_direct_subqueries(|_| {
103 found = true;
104 Ok(())
105 })?;
106 Ok(found)
107 }
108
109 /// Extracts subqueries from a conjunction into `out`.
110 ///
111 /// For example, given an expression like
112 ///
113 /// ```text
114 /// a = b AND EXISTS (<subquery 1>) AND c = d AND (<subquery 2>) = e
115 /// ```
116 ///
117 /// this function rewrites the expression to
118 ///
119 /// ```text
120 /// a = b AND true AND c = d AND true
121 /// ```
122 ///
123 /// and returns the expression fragments `EXISTS (<subquery 1>)` and
124 /// `(<subquery 2>) = e` in the `out` vector.
125 fn extract_conjuncted_subqueries(
126 expr: &mut HirScalarExpr,
127 out: &mut Vec<HirScalarExpr>,
128 ) -> Result<(), RecursionLimitError> {
129 match expr {
130 HirScalarExpr::CallVariadic {
131 func: VariadicFunc::And(_),
132 exprs,
133 name: _,
134 } => {
135 exprs
136 .into_iter()
137 .try_for_each(|e| extract_conjuncted_subqueries(e, out))?;
138 }
139 expr if contains_subquery(expr)? => {
140 out.push(mem::replace(expr, HirScalarExpr::literal_true()))
141 }
142 _ => (),
143 }
144 Ok(())
145 }
146
147 walk_relation(expr)
148}
149
150/// Rewrites quantified comparisons into simpler EXISTS operators.
151///
152/// Note that this transformation is only valid when the expression is
153/// used in a context where the distinction between `FALSE` and `NULL`
154/// is immaterial, e.g., in a `WHERE` clause or a `CASE` condition, or
155/// when the inputs to the comparison are non-nullable. This function is careful
156/// to only apply the transformation when it is valid to do so.
157///
158/// ```ignore
159/// WHERE (SELECT any(<pred>) FROM <rel>)
160/// =>
161/// WHERE EXISTS(SELECT * FROM <rel> WHERE <pred>)
162///
163/// WHERE (SELECT all(<pred>) FROM <rel>)
164/// =>
165/// WHERE NOT EXISTS(SELECT * FROM <rel> WHERE (NOT <pred>) OR <pred> IS NULL)
166/// ```
167///
168/// See Section 3.5 of "Execution Strategies for SQL Subqueries" by
169/// M. Elhemali, et al.
170pub fn try_simplify_quantified_comparisons(
171 expr: &mut HirRelationExpr,
172 simplify_join_on: bool,
173) -> Result<(), RecursionLimitError> {
174 fn walk_relation(
175 expr: &mut HirRelationExpr,
176 outers: &[SqlRelationType],
177 simplify_join_on: bool,
178 ) -> Result<(), RecursionLimitError> {
179 match expr {
180 HirRelationExpr::Map { scalars, input } => {
181 walk_relation(input, outers, simplify_join_on)?;
182 let mut outers = outers.to_vec();
183 outers.insert(0, input.typ(&outers, &NO_PARAMS));
184 for scalar in scalars {
185 walk_scalar(scalar, &outers, false, simplify_join_on)?;
186 let (inner, outers) = outers
187 .split_first_mut()
188 .expect("outers known to have at least one element");
189 let scalar_type = scalar.typ(outers, inner, &NO_PARAMS);
190 inner.column_types.push(scalar_type);
191 }
192 }
193 HirRelationExpr::Filter { predicates, input } => {
194 walk_relation(input, outers, simplify_join_on)?;
195 let mut outers = outers.to_vec();
196 outers.insert(0, input.typ(&outers, &NO_PARAMS));
197 for pred in predicates {
198 walk_scalar(pred, &outers, true, simplify_join_on)?;
199 }
200 }
201 HirRelationExpr::CallTable { exprs, .. } => {
202 let mut outers = outers.to_vec();
203 outers.insert(0, SqlRelationType::empty());
204 for scalar in exprs {
205 walk_scalar(scalar, &outers, false, simplify_join_on)?;
206 }
207 }
208 HirRelationExpr::Join {
209 left, right, on, ..
210 } => {
211 walk_relation(left, outers, simplify_join_on)?;
212 let left_type = left.typ(outers, &NO_PARAMS);
213 let mut outers = outers.to_vec();
214 outers.insert(0, left_type);
215 walk_relation(right, &outers, simplify_join_on)?;
216 if simplify_join_on {
217 // Build outers with the full join output type, since the
218 // ON clause can reference columns from both sides.
219 let right_type = right.typ(&outers, &NO_PARAMS);
220 let mut join_columns = outers[0].column_types.clone();
221 join_columns.extend(right_type.column_types);
222 outers[0] = SqlRelationType::new(join_columns);
223 walk_scalar(on, &outers, true, simplify_join_on)?;
224 }
225 }
226 expr => {
227 #[allow(deprecated)]
228 let _ = expr.visit1_mut(0, &mut |expr, _| -> Result<(), RecursionLimitError> {
229 walk_relation(expr, outers, simplify_join_on)
230 });
231 }
232 }
233 Ok(())
234 }
235
236 fn walk_scalar(
237 expr: &mut HirScalarExpr,
238 outers: &[SqlRelationType],
239 mut in_filter: bool,
240 simplify_join_on: bool,
241 ) -> Result<(), RecursionLimitError> {
242 expr.try_visit_mut_pre(&mut |e| {
243 match e {
244 HirScalarExpr::Exists(input, _name) => {
245 walk_relation(input, outers, simplify_join_on)?
246 }
247 HirScalarExpr::Select(input, _name) => {
248 walk_relation(input, outers, simplify_join_on)?;
249
250 // We're inside a `(SELECT ...)` subquery. Now let's see if
251 // it has the form `(SELECT <any|all>(...) FROM <input>)`.
252 // Ideally we could do this with one pattern, but Rust's pattern
253 // matching engine is not powerful enough, so we have to do this
254 // in stages; the early returns avoid brutal nesting.
255
256 let (func, expr, input) = match &mut **input {
257 HirRelationExpr::Reduce {
258 group_key,
259 aggregates,
260 input,
261 expected_group_size: _,
262 } if group_key.is_empty() && aggregates.len() == 1 => {
263 let agg = &mut aggregates[0];
264 (&agg.func, &mut agg.expr, input)
265 }
266 _ => return Ok(()),
267 };
268
269 if !in_filter && column_type(outers, input, expr).nullable {
270 // Unless we're directly inside a WHERE, this
271 // transformation is only valid if the expression involved
272 // is non-nullable.
273 return Ok(());
274 }
275
276 match func {
277 AggregateFunc::Any => {
278 // Found `(SELECT any(<expr>) FROM <input>)`. Rewrite to
279 // `EXISTS(SELECT 1 FROM <input> WHERE <expr>)`.
280 *e = input.take().filter(vec![expr.take()]).exists();
281 }
282 AggregateFunc::All => {
283 // Found `(SELECT all(<expr>) FROM <input>)`. Rewrite to
284 // `NOT EXISTS(SELECT 1 FROM <input> WHERE NOT <expr> OR <expr> IS NULL)`.
285 //
286 // Note that negation of <expr> alone is insufficient.
287 // Consider that `WHERE <pred>` filters out rows if
288 // `<pred>` is false *or* null. To invert the test, we
289 // need `NOT <pred> OR <pred> IS NULL`.
290 let expr = expr.take();
291 let filter = expr.clone().not().or(expr.call_is_null());
292 *e = input.take().filter(vec![filter]).exists().not();
293 }
294 _ => (),
295 }
296 }
297 _ => {
298 // As soon as we see *any* scalar expression, we are no longer
299 // directly inside a filter.
300 in_filter = false;
301 }
302 }
303 Ok(())
304 })
305 }
306
307 walk_relation(expr, &[], simplify_join_on)
308}
309
310/// Collapses `EXISTS` over a FROM-less subquery into an equivalent scalar
311/// predicate on the outer row, so that decorrelation produces a plain `Filter`
312/// rather than a semijoin (for `EXISTS`) or an antijoin (for `NOT EXISTS`).
313///
314/// A FROM-less subquery is a chain of `Map`, `Project`, and `Filter` nodes over
315/// a single-row `Constant` (the join identity of a query with no `FROM`
316/// clause). Such a subquery yields exactly one row when every `Filter`
317/// predicate is `TRUE` and zero rows otherwise, so
318///
319/// ```text
320/// EXISTS(<from-less subquery with predicates p1, p2, ...>) == (p1 AND p2 AND ...) IS TRUE
321/// ```
322///
323/// evaluated on the outer row. The `IS TRUE` is mandatory for null safety. An
324/// empty subquery (some predicate `FALSE` or `NULL`) must make `EXISTS` return
325/// `FALSE`, which `IS TRUE` reproduces while a bare predicate would leak `NULL`.
326/// `NOT EXISTS` then becomes `NOT ((...) IS TRUE)`, which is `... IS NOT TRUE`
327/// and likewise null-safe.
328///
329/// The rewrite fires only on correlated subqueries, where the predicate
330/// references at least one outer column. This keeps it to the pure existence
331/// check that a genuine anti/semi-join would otherwise be lowered to, and it
332/// avoids changing whether an uncorrelated erroring subquery is evaluated when
333/// the outer relation is empty.
334///
335/// This closes database-issues#2613 (`x IN (SELECT ... WHERE p)`, which
336/// [`try_simplify_quantified_comparisons`] has already turned into an `EXISTS`)
337/// and database-issues#2969 (`NOT EXISTS (SELECT ... WHERE p)`). It must run
338/// after [`try_simplify_quantified_comparisons`].
339pub fn simplify_from_less_existence_subqueries(
340 expr: &mut HirRelationExpr,
341) -> Result<(), RecursionLimitError> {
342 // `try_visit_mut_post` walks every relation node, and because
343 // `VisitChildren<Self>` for `HirRelationExpr` descends into the bodies of
344 // `Exists`/`Select` subqueries, it reaches existence checks at every nesting
345 // level. Post-order guarantees a subquery body is simplified before the
346 // `Exists` that encloses it.
347 expr.try_visit_mut_post(&mut |rel| {
348 rel.try_visit_mut_children(|scalar: &mut HirScalarExpr| {
349 scalar.try_visit_mut_pre(&mut |e| {
350 if let HirScalarExpr::Exists(input, _name) = e {
351 if let Some(pred) = from_less_existence_predicate(input) {
352 *e = pred.call_unary(UnaryFunc::IsTrue(mz_expr::func::IsTrue));
353 }
354 }
355 Ok(())
356 })
357 })
358 })
359}
360
361/// If `sub` is a FROM-less subquery (see
362/// [`simplify_from_less_existence_subqueries`]) whose existence check is
363/// correlated on the outer row, returns the predicate `p1 AND p2 AND ...`
364/// expressed in the outer row's frame. Returns `None` otherwise.
365fn from_less_existence_predicate(sub: &HirRelationExpr) -> Option<HirScalarExpr> {
366 // A FROM-less subquery is a linear Map/Project/Filter chain over a single-row
367 // `Constant`. Both properties of the base are load-bearing for soundness: the
368 // single row is what lets EXISTS reduce to "the predicate holds on that row",
369 // and the constant is what lets its columns be inlined into the lifted
370 // predicate below. A 0-row, multi-row, or non-constant base is a genuine
371 // anti/semi-join and bails at the `_` arm.
372 //
373 // Record the chain top to bottom here; it is replayed bottom to top below.
374 let mut chain = Vec::new();
375 let mut cur = sub;
376 let (row, typ) = loop {
377 match cur {
378 HirRelationExpr::Filter { input, .. }
379 | HirRelationExpr::Map { input, .. }
380 | HirRelationExpr::Project { input, .. } => {
381 chain.push(cur);
382 cur = input.as_ref();
383 }
384 HirRelationExpr::Constant { rows, typ } if rows.len() == 1 => break (&rows[0], typ),
385 _ => return None,
386 }
387 };
388
389 // `env` holds the value of each column of the current relation, expressed in
390 // the subquery's own frame. Because level-0 references are resolved as we go,
391 // env entries only ever contain constants and outer (level >= 1) references.
392 let mut env: Vec<HirScalarExpr> = row
393 .iter()
394 .zip_eq(typ.column_types.iter())
395 .map(|(datum, col_type)| HirScalarExpr::literal(datum, col_type.scalar_type.clone()))
396 .collect();
397
398 // Replay the chain bottom to top so each node sees the `env` built by the nodes
399 // beneath it: `Map` extends `env`, `Filter` reads it, `Project` permutes it.
400 let mut preds: Vec<HirScalarExpr> = Vec::new();
401 for node in chain.iter().rev() {
402 match node {
403 HirRelationExpr::Filter { predicates, .. } => {
404 for predicate in predicates {
405 preds.push(resolve_local_columns(predicate, &env)?);
406 }
407 }
408 HirRelationExpr::Map { scalars, .. } => {
409 for scalar in scalars {
410 let resolved = resolve_local_columns(scalar, &env)?;
411 env.push(resolved);
412 }
413 }
414 HirRelationExpr::Project { outputs, .. } => {
415 env = outputs
416 .iter()
417 .map(|i| env.get(*i).cloned())
418 .collect::<Option<Vec<_>>>()?;
419 }
420 _ => unreachable!("chain only contains Filter, Map, and Project nodes"),
421 }
422 }
423
424 let mut pred = HirScalarExpr::variadic_and(preds);
425
426 // `pred` is built only from predicates that `resolve_local_columns` accepted,
427 // and that rejects any subquery, so `pred` contains no nested subqueries. Every
428 // column reference is therefore in the subquery's own frame at nesting depth 0,
429 // and an outer reference is exactly one with `level > 0`.
430
431 // Require correlation: the predicate must reference an outer column. Without
432 // correlation this is not the existence check a genuine anti/semi-join lowers
433 // to, and firing would risk changing when a constant erroring predicate is
434 // evaluated.
435 let mut correlated = false;
436 pred.visit_post(&mut |e| {
437 if let HirScalarExpr::Column(col, _name) = e {
438 if col.level > 0 {
439 correlated = true;
440 }
441 }
442 });
443 if !correlated {
444 return None;
445 }
446
447 // Lift the predicate out of the subquery: references to the immediately
448 // enclosing (outer) scope move down one level.
449 pred.visit_mut_post(&mut |e| {
450 if let HirScalarExpr::Column(col, _name) = e {
451 if col.level > 0 {
452 col.level -= 1;
453 }
454 }
455 });
456
457 Some(pred)
458}
459
460/// Returns `expr` with every reference to the current scope (a [`ColumnRef`]
461/// with `level == 0`) replaced by its value from `env`. Returns `None` if `expr`
462/// cannot be soundly lifted into the outer scope, or references a column absent
463/// from `env`.
464fn resolve_local_columns(expr: &HirScalarExpr, env: &[HirScalarExpr]) -> Option<HirScalarExpr> {
465 // Every scalar in the FROM-less body is substituted into the outer scope, so
466 // reject any that cannot be evaluated equivalently there. The match is
467 // exhaustive on purpose: a new `HirScalarExpr` variant must be classified here
468 // rather than silently treated as liftable.
469 let mut unliftable = false;
470 expr.visit_post(&mut |e| {
471 let liftable = match e {
472 // Row-local: the value depends only on the row, so it is the same in
473 // the subquery's frame and the outer frame.
474 HirScalarExpr::Column(..)
475 | HirScalarExpr::Parameter(..)
476 | HirScalarExpr::Literal(..)
477 | HirScalarExpr::CallUnmaterializable(..)
478 | HirScalarExpr::CallUnary { .. }
479 | HirScalarExpr::CallBinary { .. }
480 | HirScalarExpr::CallVariadic { .. }
481 | HirScalarExpr::If { .. } => true,
482 // A subquery carries its own nested scopes that this flat substitution
483 // does not handle. A window function over the single-row body (e.g.
484 // `row_number() OVER ()` is always 1) is not the same function over the
485 // multi-row outer relation. Neither may cross the subquery boundary.
486 HirScalarExpr::Exists(..)
487 | HirScalarExpr::Select(..)
488 | HirScalarExpr::Windowing(..) => false,
489 };
490 unliftable |= !liftable;
491 });
492 if unliftable {
493 return None;
494 }
495
496 let mut expr = expr.clone();
497 let mut ok = true;
498 expr.visit_mut_post(&mut |e| {
499 if let HirScalarExpr::Column(ColumnRef { level: 0, column }, _name) = e {
500 match env.get(*column) {
501 Some(value) => *e = value.clone(),
502 None => ok = false,
503 }
504 }
505 });
506 ok.then_some(expr)
507}
508
509/// An empty parameter type map.
510///
511/// These transformations are expected to run after parameters are bound, so
512/// there is no need to provide any parameter type information.
513static NO_PARAMS: LazyLock<BTreeMap<usize, SqlScalarType>> = LazyLock::new(BTreeMap::new);
514
515fn column_type(
516 outers: &[SqlRelationType],
517 inner: &HirRelationExpr,
518 expr: &HirScalarExpr,
519) -> SqlColumnType {
520 let inner_type = inner.typ(outers, &NO_PARAMS);
521 expr.typ(outers, &inner_type, &NO_PARAMS)
522}
523
524impl HirScalarExpr {
525 /// Similar to `MirScalarExpr::support`, but adapted to `HirScalarExpr` in a special way: it
526 /// considers column references that target the root level.
527 /// (See `visit_columns_referring_to_root_level`.)
528 fn support(&self) -> Vec<usize> {
529 let mut result = Vec::new();
530 self.visit_columns_referring_to_root_level(&mut |c| result.push(c));
531 result
532 }
533
534 /// Changes column references in `self` by the given remapping.
535 /// Panics if a referred column is not present in `idx_map`!
536 fn remap(mut self, idx_map: &BTreeMap<usize, usize>) -> HirScalarExpr {
537 self.visit_columns_referring_to_root_level_mut(&mut |c| {
538 *c = idx_map[c];
539 });
540 self
541 }
542}
543
544/// # Aims and scope
545///
546/// The aim here is to amortize the overhead of the MIR window function pattern
547/// (see `window_func_applied_to`) by fusing groups of window function calls such
548/// that each group can be performed by one instance of the window function MIR
549/// pattern.
550///
551/// For now, we fuse only value window function calls and window aggregations.
552/// (We probably won't need to fuse scalar window functions for a long time.)
553///
554/// For now, we can fuse value window function calls and window aggregations where the
555/// A. partition by
556/// B. order by
557/// C. window frame
558/// D. ignore nulls for value window functions and distinct for window aggregations
559/// are all the same. (See `extract_options`.)
560/// (Later, we could improve this to only need A. to be the same. This would require
561/// much more code changes, because then we'd have to blow up `ValueWindowExpr`.
562/// TODO: As a much simpler intermediate step, at least we should ignore options that
563/// don't matter. For example, we should be able to fuse a `lag` that has a default
564/// frame with a `first_value` that has some custom frame, because `lag` is not
565/// affected by the frame.)
566/// Note that we fuse value window function calls and window aggregations separately.
567///
568/// # Implementation
569///
570/// At a high level, what we are going to do is look for Maps with more than one window function
571/// calls, and for each Map
572/// - remove some groups of window function call expressions from the Map's `scalars`;
573/// - insert a fused version of each group;
574/// - insert some expressions that decompose the results of the fused calls;
575/// - update some column references in `scalars`: those that refer to window function results that
576/// participated in fusion, as well as those that refer to columns that moved around due to
577/// removing and inserting expressions.
578/// - insert a Project above the matched Map to permute columns back to their original places.
579///
580/// It would be tempting to find groups simply by taking a list of all window function calls
581/// and calling `group_by` with a key function that extracts the above A. B. C. D. properties,
582/// but a complication is that the possible groups that we could theoretically fuse overlap.
583/// This is because when forming groups we need to also take into account column references
584/// that point inside the same Map. For example, imagine a Map with the following scalar
585/// expressions:
586/// C1, E1, C2, C3, where
587/// - E1 refers to C1
588/// - C3 refers to E1.
589/// In this situation, we could either
590/// - fuse C1 and C2, and put the fused expression in the place of C1 (so that E1 can keep referring
591/// to it);
592/// - or fuse C2 and C3.
593/// However, we can't fuse all of C1, C2, C3 into one call, because then there would be
594/// no appropriate place for the fused expression: it would have to be both before and after E1.
595///
596/// So, how we actually form the groups is that, keeping track of a list of non-overlapping groups,
597/// we go through `scalars`, try to put each expression into each of our groups, and the first of
598/// these succeed. When trying to put an expression into a group, we need to be mindful about column
599/// references inside the same Map, as noted above. A constraint that we impose on ourselves for
600/// sanity is that the fused version of each group will be inserted at the place where the first
601/// element of the group originally was. This means that the only condition that we need to check on
602/// column references when adding an expression to a group is that all column references in a group
603/// should be to columns that are earlier than the first element of the group. (No need to check
604/// column references in the other direction, i.e., references in other expressions that refer to
605/// columns in the group.)
606pub fn fuse_window_functions(
607 root: &mut HirRelationExpr,
608 _context: &crate::plan::lowering::Context,
609) -> Result<(), RecursionLimitError> {
610 /// Those options of a window function call that are relevant for fusion.
611 #[derive(PartialEq, Eq)]
612 enum WindowFuncCallOptions {
613 Value(ValueWindowFuncCallOptions),
614 Agg(AggregateWindowFuncCallOptions),
615 }
616 #[derive(PartialEq, Eq)]
617 struct ValueWindowFuncCallOptions {
618 partition_by: Vec<HirScalarExpr>,
619 outer_order_by: Vec<HirScalarExpr>,
620 inner_order_by: Vec<ColumnOrder>,
621 window_frame: WindowFrame,
622 ignore_nulls: bool,
623 }
624 #[derive(PartialEq, Eq)]
625 struct AggregateWindowFuncCallOptions {
626 partition_by: Vec<HirScalarExpr>,
627 outer_order_by: Vec<HirScalarExpr>,
628 inner_order_by: Vec<ColumnOrder>,
629 window_frame: WindowFrame,
630 distinct: bool,
631 }
632
633 /// Helper function to extract the above options.
634 fn extract_options(call: &HirScalarExpr) -> WindowFuncCallOptions {
635 match call {
636 HirScalarExpr::Windowing(
637 WindowExpr {
638 func:
639 WindowExprType::Value(ValueWindowExpr {
640 order_by: inner_order_by,
641 window_frame,
642 ignore_nulls,
643 func: _,
644 args: _,
645 }),
646 partition_by,
647 order_by: outer_order_by,
648 },
649 _name,
650 ) => WindowFuncCallOptions::Value(ValueWindowFuncCallOptions {
651 partition_by: partition_by.clone(),
652 outer_order_by: outer_order_by.clone(),
653 inner_order_by: inner_order_by.clone(),
654 window_frame: window_frame.clone(),
655 ignore_nulls: ignore_nulls.clone(),
656 }),
657 HirScalarExpr::Windowing(
658 WindowExpr {
659 func:
660 WindowExprType::Aggregate(AggregateWindowExpr {
661 aggregate_expr:
662 AggregateExpr {
663 distinct,
664 func: _,
665 expr: _,
666 },
667 order_by: inner_order_by,
668 window_frame,
669 }),
670 partition_by,
671 order_by: outer_order_by,
672 },
673 _name,
674 ) => WindowFuncCallOptions::Agg(AggregateWindowFuncCallOptions {
675 partition_by: partition_by.clone(),
676 outer_order_by: outer_order_by.clone(),
677 inner_order_by: inner_order_by.clone(),
678 window_frame: window_frame.clone(),
679 distinct: distinct.clone(),
680 }),
681 _ => panic!(
682 "extract_options should only be called on value window functions or window aggregations"
683 ),
684 }
685 }
686
687 struct FusionGroup {
688 /// The original column index of the first element of the group. (This is an index into the
689 /// Map's `scalars` plus the arity of the Map's input.)
690 first_col: usize,
691 /// The options of all the window function calls in the group. (Must be the same for all the
692 /// calls.)
693 options: WindowFuncCallOptions,
694 /// The calls in the group, with their original column indexes.
695 calls: Vec<(usize, HirScalarExpr)>,
696 }
697
698 impl FusionGroup {
699 /// Creates a window function call that is a fused version of all the calls in the group.
700 /// `new_col` is the column index where the fused call will be inserted at.
701 fn fuse(self, new_col: usize) -> (HirScalarExpr, Vec<HirScalarExpr>) {
702 let fused = match self.options {
703 WindowFuncCallOptions::Value(options) => {
704 let (fused_funcs, fused_args): (Vec<_>, Vec<_>) = self
705 .calls
706 .iter()
707 .map(|(_idx, call)| {
708 if let HirScalarExpr::Windowing(
709 WindowExpr {
710 func:
711 WindowExprType::Value(ValueWindowExpr {
712 func,
713 args,
714 order_by: _,
715 window_frame: _,
716 ignore_nulls: _,
717 }),
718 partition_by: _,
719 order_by: _,
720 },
721 _name,
722 ) = call
723 {
724 (func.clone(), (**args).clone())
725 } else {
726 panic!("unknown window function in FusionGroup")
727 }
728 })
729 .unzip();
730 let fused_args = HirScalarExpr::call_variadic(
731 RecordCreate {
732 // These field names are not important, because this record will only be an
733 // intermediate expression, which we'll manipulate further before it ends up
734 // anywhere where a column name would be visible.
735 field_names: iter::repeat(ColumnName::from(""))
736 .take(fused_args.len())
737 .collect(),
738 },
739 fused_args,
740 );
741 HirScalarExpr::windowing(WindowExpr {
742 func: WindowExprType::Value(ValueWindowExpr {
743 func: ValueWindowFunc::Fused(fused_funcs),
744 args: Box::new(fused_args),
745 order_by: options.inner_order_by,
746 window_frame: options.window_frame,
747 ignore_nulls: options.ignore_nulls,
748 }),
749 partition_by: options.partition_by,
750 order_by: options.outer_order_by,
751 })
752 }
753 WindowFuncCallOptions::Agg(options) => {
754 let (fused_funcs, fused_args): (Vec<_>, Vec<_>) = self
755 .calls
756 .iter()
757 .map(|(_idx, call)| {
758 if let HirScalarExpr::Windowing(
759 WindowExpr {
760 func:
761 WindowExprType::Aggregate(AggregateWindowExpr {
762 aggregate_expr:
763 AggregateExpr {
764 func,
765 expr,
766 distinct: _,
767 },
768 order_by: _,
769 window_frame: _,
770 }),
771 partition_by: _,
772 order_by: _,
773 },
774 _name,
775 ) = call
776 {
777 (func.clone(), (**expr).clone())
778 } else {
779 panic!("unknown window function in FusionGroup")
780 }
781 })
782 .unzip();
783 let fused_args = HirScalarExpr::call_variadic(
784 RecordCreate {
785 field_names: iter::repeat(ColumnName::from(""))
786 .take(fused_args.len())
787 .collect(),
788 },
789 fused_args,
790 );
791 HirScalarExpr::windowing(WindowExpr {
792 func: WindowExprType::Aggregate(AggregateWindowExpr {
793 aggregate_expr: AggregateExpr {
794 func: AggregateFunc::FusedWindowAgg { funcs: fused_funcs },
795 expr: Box::new(fused_args),
796 distinct: options.distinct,
797 },
798 order_by: options.inner_order_by,
799 window_frame: options.window_frame,
800 }),
801 partition_by: options.partition_by,
802 order_by: options.outer_order_by,
803 })
804 }
805 };
806
807 let decompositions = (0..self.calls.len())
808 .map(|field| {
809 HirScalarExpr::column(new_col)
810 .call_unary(UnaryFunc::RecordGet(mz_expr::func::RecordGet(field)))
811 })
812 .collect();
813
814 (fused, decompositions)
815 }
816 }
817
818 let is_value_or_agg_window_func_call = |scalar_expr: &HirScalarExpr| -> bool {
819 // Look for calls only at the root of scalar expressions. This is enough
820 // because they are always there, see 72e84bb78.
821 match scalar_expr {
822 HirScalarExpr::Windowing(
823 WindowExpr {
824 func: WindowExprType::Value(ValueWindowExpr { func, .. }),
825 ..
826 },
827 _name,
828 ) => {
829 // Exclude those calls that are already fused. (We shouldn't currently
830 // encounter these, because we just do one pass, but it's better to be
831 // robust against future code changes.)
832 !matches!(func, ValueWindowFunc::Fused(..))
833 }
834 HirScalarExpr::Windowing(
835 WindowExpr {
836 func:
837 WindowExprType::Aggregate(AggregateWindowExpr {
838 aggregate_expr: AggregateExpr { func, .. },
839 ..
840 }),
841 ..
842 },
843 _name,
844 ) => !matches!(func, AggregateFunc::FusedWindowAgg { .. }),
845 _ => false,
846 }
847 };
848
849 root.try_visit_mut_post(&mut |rel_expr| {
850 match rel_expr {
851 HirRelationExpr::Map { input, scalars } => {
852 // There will be various variable names involving `idx` or `col`:
853 // - `idx` will always be an index into `scalars` or something similar,
854 // - `col` will always be a column index,
855 // which is often `arity_before_map` + an index into `scalars`.
856 let arity_before_map = input.arity();
857 let orig_num_scalars = scalars.len();
858
859 // Collect all value window function calls and window aggregations with their column
860 // indexes.
861 let value_or_agg_window_func_calls = scalars
862 .iter()
863 .enumerate()
864 .filter(|(_idx, scalar_expr)| is_value_or_agg_window_func_call(scalar_expr))
865 .map(|(idx, call)| (idx + arity_before_map, call.clone()))
866 .collect_vec();
867 // Exit early if obviously no chance for fusion.
868 if value_or_agg_window_func_calls.len() <= 1 {
869 // Note that we are doing this only for performance. All plans should be exactly
870 // the same even if we comment out the following line.
871 return Ok(());
872 }
873
874 // Determine the fusion groups. (Each group will later be fused into one window
875 // function call.)
876 // Note that this has a quadratic run time with value_or_agg_window_func_calls in
877 // the worst case. However, this is fine even with 1000 window function calls.
878 let mut groups: Vec<FusionGroup> = Vec::new();
879 for (col, call) in value_or_agg_window_func_calls {
880 let options = extract_options(&call);
881 let support = call.support();
882 let to_fuse_with = groups
883 .iter_mut()
884 .filter(|group| {
885 group.options == options && support.iter().all(|c| *c < group.first_col)
886 })
887 .next();
888 if let Some(group) = to_fuse_with {
889 group.calls.push((col, call.clone()));
890 } else {
891 groups.push(FusionGroup {
892 first_col: col,
893 options,
894 calls: vec![(col, call.clone())],
895 });
896 }
897 }
898
899 // No fusion to do on groups of 1.
900 groups.retain(|g| g.calls.len() > 1);
901
902 let removals: BTreeSet<usize> = groups
903 .iter()
904 .flat_map(|g| g.calls.iter().map(|(col, _)| *col))
905 .collect();
906
907 // Mutate `scalars`.
908 // We do this by simultaneously iterating through `scalars` and `groups`. (Note that
909 // `groups` is already sorted by `first_col` due to the way it was constructed.)
910 // We also compute a remapping of old indexes to new indexes as we go.
911 let mut groups_it = groups.drain(..).peekable();
912 let mut group = groups_it.next();
913 let mut remap = BTreeMap::new();
914 remap.extend((0..arity_before_map).map(|col| (col, col)));
915 let mut new_col: usize = arity_before_map;
916 let mut new_scalars = Vec::new();
917 for (old_col, e) in scalars
918 .drain(..)
919 .enumerate()
920 .map(|(idx, e)| (idx + arity_before_map, e))
921 {
922 if group.as_ref().is_some_and(|g| g.first_col == old_col) {
923 // The current expression will be fused away, and a fused expression will
924 // appear in its place. Additionally, some new expressions will be inserted
925 // after the fused expression, to decompose the record that is the result of
926 // the fused call.
927 assert!(removals.contains(&old_col));
928 let group_unwrapped = group.expect("checked above");
929 let calls_cols = group_unwrapped
930 .calls
931 .iter()
932 .map(|(col, _call)| *col)
933 .collect_vec();
934 let (fused, decompositions) = group_unwrapped.fuse(new_col);
935 new_scalars.push(fused.remap(&remap));
936 new_scalars.extend(decompositions); // (no remapping needed)
937 new_col += 1;
938 for call_old_col in calls_cols {
939 let present = remap.insert(call_old_col, new_col);
940 assert!(present.is_none());
941 new_col += 1;
942 }
943 group = groups_it.next();
944 } else if removals.contains(&old_col) {
945 assert!(remap.contains_key(&old_col));
946 } else {
947 new_scalars.push(e.remap(&remap));
948 let present = remap.insert(old_col, new_col);
949 assert!(present.is_none());
950 new_col += 1;
951 }
952 }
953 *scalars = new_scalars;
954 assert_eq!(remap.len(), arity_before_map + orig_num_scalars);
955
956 // Add a project to permute columns back to their original places.
957 *rel_expr = rel_expr.take().project(
958 (0..arity_before_map)
959 .chain((0..orig_num_scalars).map(|idx| {
960 *remap
961 .get(&(idx + arity_before_map))
962 .expect("all columns should be present by now")
963 }))
964 .collect(),
965 );
966
967 assert_eq!(rel_expr.arity(), arity_before_map + orig_num_scalars);
968 }
969 _ => {}
970 }
971 Ok(())
972 })
973}