mz_transform/lib.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 for relation expressions.
11//!
12//! This crate contains traits, types, and methods suitable for transforming
13//! `MirRelationExpr` types in ways that preserve semantics and improve performance.
14//! The core trait is `Transform`, and many implementors of this trait can be
15//! boxed and iterated over. Some common transformation patterns are wrapped
16//! as `Transform` implementors themselves.
17//!
18//! The crate also contains the beginnings of whole-dataflow optimization,
19//! which uses the same analyses but spanning multiple dataflow elements.
20
21#![warn(missing_docs)]
22#![warn(missing_debug_implementations)]
23
24use std::collections::BTreeMap;
25use std::error::Error;
26use std::panic::AssertUnwindSafe;
27use std::sync::Arc;
28use std::{fmt, iter};
29
30use mz_expr::{MirRelationExpr, MirScalarExpr};
31use mz_ore::id_gen::IdGen;
32use mz_ore::soft_panic_or_log;
33use mz_ore::stack::RecursionLimitError;
34use mz_repr::GlobalId;
35use mz_repr::optimize::OptimizerFeatures;
36use mz_sql::optimizer_metrics::OptimizerMetrics;
37use tracing::error;
38
39use crate::canonicalize_mfp::CanonicalizeMfp;
40use crate::collect_notices::CollectNotices;
41use crate::column_knowledge::ColumnKnowledge;
42use crate::dataflow::DataflowMetainfo;
43use crate::demand::Demand;
44use crate::equivalence_propagation::EquivalencePropagation;
45use crate::fold_constants::FoldConstants;
46use crate::join_implementation::JoinImplementation;
47use crate::literal_constraints::LiteralConstraints;
48use crate::literal_lifting::LiteralLifting;
49use crate::movement::ProjectionPushdown;
50use crate::non_null_requirements::NonNullRequirements;
51use crate::normalize_lets::NormalizeLets;
52use crate::normalize_ops::NormalizeOps;
53use crate::predicate_pushdown::PredicatePushdown;
54use crate::reduce_elision::ReduceElision;
55use crate::reduce_reduction::ReduceReduction;
56use crate::reduction_pushdown::ReductionPushdown;
57use crate::redundant_join::RedundantJoin;
58use crate::semijoin_idempotence::SemijoinIdempotence;
59use crate::threshold_elision::ThresholdElision;
60use crate::typecheck::{SharedTypecheckingContext, Typecheck};
61use crate::union_cancel::UnionBranchCancellation;
62use crate::will_distinct::WillDistinct;
63
64pub use dataflow::optimize_dataflow;
65
66pub mod analysis;
67pub mod canonicalization;
68pub mod canonicalize_mfp;
69pub mod case_literal;
70pub mod coalesce_case;
71pub mod collect_notices;
72pub mod column_knowledge;
73pub mod compound;
74pub mod cse;
75pub mod dataflow;
76pub mod demand;
77pub mod equivalence_propagation;
78pub mod fold_constants;
79pub mod fusion;
80pub mod join_implementation;
81pub mod literal_constraints;
82pub mod literal_lifting;
83pub mod monotonic;
84pub mod movement;
85pub mod non_null_requirements;
86pub mod normalize_lets;
87pub mod normalize_ops;
88pub mod notice;
89pub mod ordering;
90pub mod predicate_pushdown;
91pub mod reduce_elision;
92pub mod reduce_reduction;
93pub mod reduction_pushdown;
94pub mod redundant_join;
95pub mod semijoin_idempotence;
96pub mod threshold_elision;
97pub mod typecheck;
98pub mod union_cancel;
99pub mod will_distinct;
100
101/// Compute the conjunction of a variadic number of expressions.
102#[macro_export]
103macro_rules! all {
104 ($x:expr) => ($x);
105 ($($x:expr,)+) => ( $($x)&&+ )
106}
107
108/// Compute the disjunction of a variadic number of expressions.
109#[macro_export]
110macro_rules! any {
111 ($x:expr) => ($x);
112 ($($x:expr,)+) => ( $($x)||+ )
113}
114
115/// Arguments that get threaded through all transforms, plus a `DataflowMetainfo` that can be
116/// manipulated by the transforms.
117#[derive(Debug)]
118pub struct TransformCtx<'a> {
119 /// The global ID for this query (if it exists).
120 pub global_id: Option<GlobalId>,
121 /// The indexes accessible.
122 pub indexes: &'a dyn IndexOracle,
123 /// Statistical estimates.
124 pub stats: &'a dyn StatisticsOracle,
125 /// Features passed to the enclosing `Optimizer`.
126 pub features: &'a OptimizerFeatures,
127 /// Representation typechecking context.
128 pub typechecking_ctx: &'a SharedTypecheckingContext,
129 /// Transforms can use this field to communicate information outside the result plans.
130 pub df_meta: &'a mut DataflowMetainfo,
131 /// Metrics for the optimizer.
132 pub metrics: Option<&'a mut OptimizerMetrics>,
133 /// The last hash of the query, if known.
134 pub last_hash: BTreeMap<GlobalId, u64>,
135}
136
137const FOLD_CONSTANTS_LIMIT: usize = 10000;
138
139impl<'a> TransformCtx<'a> {
140 /// Generates a [`TransformCtx`] instance for the local MIR optimization
141 /// stage.
142 ///
143 /// Used to call [`Optimizer::optimize`] on a
144 /// [`Optimizer::logical_optimizer`] in order to transform a stand-alone
145 /// [`MirRelationExpr`].
146 pub fn local(
147 features: &'a OptimizerFeatures,
148 typecheck_ctx: &'a SharedTypecheckingContext,
149 df_meta: &'a mut DataflowMetainfo,
150 metrics: Option<&'a mut OptimizerMetrics>,
151 global_id: Option<GlobalId>,
152 ) -> Self {
153 Self {
154 indexes: &EmptyIndexOracle,
155 stats: &EmptyStatisticsOracle,
156 global_id,
157 features,
158 typechecking_ctx: typecheck_ctx,
159 df_meta,
160 metrics,
161 last_hash: Default::default(),
162 }
163 }
164
165 /// Generates a [`TransformCtx`] instance for the global MIR optimization
166 /// stage.
167 ///
168 /// Used to call [`optimize_dataflow`].
169 pub fn global(
170 indexes: &'a dyn IndexOracle,
171 stats: &'a dyn StatisticsOracle,
172 features: &'a OptimizerFeatures,
173 typecheck_ctx: &'a SharedTypecheckingContext,
174 df_meta: &'a mut DataflowMetainfo,
175 metrics: Option<&'a mut OptimizerMetrics>,
176 ) -> Self {
177 Self {
178 indexes,
179 stats,
180 global_id: None,
181 features,
182 df_meta,
183 typechecking_ctx: typecheck_ctx,
184 metrics,
185 last_hash: Default::default(),
186 }
187 }
188
189 fn typechecking_context(&self) -> SharedTypecheckingContext {
190 Arc::clone(self.typechecking_ctx)
191 }
192
193 /// Lets self know the id of the object that is being optimized.
194 pub fn set_global_id(&mut self, global_id: GlobalId) {
195 self.global_id = Some(global_id);
196 }
197
198 fn reset_global_id(&mut self) {
199 self.global_id = None;
200 }
201
202 /// Updates `last_hash` with the hash of the given MIR plan for the id `self.global_id`.
203 /// Returns the hash.
204 fn update_last_hash(&mut self, plan: &MirRelationExpr) -> u64 {
205 let hash = plan.hash_to_u64();
206 if let Some(id) = self.global_id {
207 self.last_hash.insert(id, hash);
208 }
209 hash
210 }
211}
212
213/// Types capable of transforming relation expressions.
214pub trait Transform: fmt::Debug {
215 /// Transforms a relation into a functionally equivalent relation.
216 ///
217 /// This is a wrapper around `actually_perform_transform` that also
218 /// measures the time taken and updates the optimizer metrics.
219 fn transform(
220 &self,
221 relation: &mut MirRelationExpr,
222 args: &mut TransformCtx,
223 ) -> Result<(), TransformError> {
224 let hash_before = args
225 .global_id
226 .and_then(|id| args.last_hash.get(&id).copied())
227 .unwrap_or_else(|| relation.hash_to_u64());
228
229 mz_ore::soft_assert_eq_no_log!(hash_before, relation.hash_to_u64(), "cached hash clash");
230 // actually run the transform, recording the time taken
231 let start = std::time::Instant::now();
232 let res = self.actually_perform_transform(relation, args);
233 let duration = start.elapsed();
234
235 let hash_after = args.update_last_hash(relation);
236 if let Some(metrics) = &mut args.metrics {
237 let transform_name = self.name();
238 metrics.observe_transform_time(transform_name, duration);
239 metrics.inc_transform(hash_before != hash_after, transform_name);
240 }
241
242 res
243 }
244
245 /// Transform a relation into a functionally equivalent relation.
246 ///
247 /// You transform should implement this method, but users should call
248 /// `transform` instead.
249 fn actually_perform_transform(
250 &self,
251 relation: &mut MirRelationExpr,
252 ctx: &mut TransformCtx,
253 ) -> Result<(), TransformError>;
254
255 /// A string describing the transform.
256 ///
257 /// This is useful mainly when iterating through many `Box<Transform>`
258 /// and one wants to judge progress before some defect occurs.
259 fn debug(&self) -> String {
260 format!("{:?}", self)
261 }
262
263 /// A short string naming the transform, as it will be reported in metrics.
264 fn name(&self) -> &'static str;
265}
266
267/// Errors that can occur during a transformation.
268#[derive(Debug, Clone)]
269pub enum TransformError {
270 /// An unstructured error.
271 Internal(String),
272 /// A reference to an apparently unbound identifier.
273 IdentifierMissing(mz_expr::LocalId),
274 /// Notify the caller to panic with the given message.
275 ///
276 /// This is used to bypass catch_unwind-wrapped calls of the optimizer and
277 /// support `SELECT mz_unsafe.mz_panic(<literal>)` statements as a mechanism to kill
278 /// environmentd in various tests.
279 CallerShouldPanic(String),
280}
281
282impl fmt::Display for TransformError {
283 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
284 match self {
285 TransformError::Internal(msg) => write!(f, "internal transform error: {}", msg),
286 TransformError::IdentifierMissing(i) => {
287 write!(f, "apparently unbound identifier: {:?}", i)
288 }
289 TransformError::CallerShouldPanic(msg) => {
290 write!(f, "caller should panic with message: {}", msg)
291 }
292 }
293 }
294}
295
296impl Error for TransformError {}
297
298impl From<RecursionLimitError> for TransformError {
299 fn from(error: RecursionLimitError) -> Self {
300 TransformError::Internal(error.to_string())
301 }
302}
303
304/// Implemented by error types that sometimes want to indicate that an error should cause a panic
305/// even in a `catch_unwind` context. Useful for implementing `mz_unsafe.mz_panic('forced panic')`.
306pub trait MaybeShouldPanic {
307 /// Whether the error means that we want a panic. If yes, then returns the error msg.
308 fn should_panic(&self) -> Option<String>;
309}
310
311impl MaybeShouldPanic for TransformError {
312 fn should_panic(&self) -> Option<String> {
313 match self {
314 TransformError::CallerShouldPanic(msg) => Some(msg.to_string()),
315 _ => None,
316 }
317 }
318}
319
320/// Catch panics in the given optimization, and demote them to [`TransformError::Internal`] error.
321///
322/// Additionally, if the result of the optimization is an error (not a panic) that indicates we
323/// should panic, then panic.
324pub fn catch_unwind_optimize<Opt, To, E>(optimization: Opt) -> Result<To, E>
325where
326 Opt: FnOnce() -> Result<To, E>,
327 E: From<TransformError> + MaybeShouldPanic,
328{
329 match mz_ore::panic::catch_unwind_str(AssertUnwindSafe(optimization)) {
330 Ok(Err(e)) if e.should_panic().is_some() => {
331 // Promote a `CallerShouldPanic` error from the result to a proper panic. This is
332 // needed in order to ensure that `mz_unsafe.mz_panic('forced panic')` calls still
333 // panic the caller.
334 panic!("{}", e.should_panic().expect("checked above"));
335 }
336 Ok(result) => result.map_err(|e| e),
337 Err(panic) => {
338 // A panic during optimization is always a bug; log an error so we learn about it.
339 // TODO(teskje): collect and log a backtrace from the panic site
340 tracing::error!("caught a panic during query optimization: {panic}");
341
342 let msg = format!("unexpected panic during query optimization: {panic}");
343 Err(TransformError::Internal(msg).into())
344 }
345 }
346}
347
348/// A trait for a type that can answer questions about what indexes exist.
349pub trait IndexOracle: fmt::Debug {
350 /// Returns an iterator over the indexes that exist on the identified
351 /// collection.
352 ///
353 /// Each index is described by the list of key expressions. If no indexes
354 /// exist for the identified collection, or if the identified collection
355 /// is unknown, the returned iterator will be empty.
356 ///
357 // NOTE(benesch): The allocation here is unfortunate, but on the other hand
358 // you need only allocate when you actually look for an index. Can we do
359 // better somehow? Making the entire optimizer generic over this iterator
360 // type doesn't presently seem worthwhile.
361 fn indexes_on(
362 &self,
363 id: GlobalId,
364 ) -> Box<dyn Iterator<Item = (GlobalId, &[MirScalarExpr])> + '_>;
365}
366
367/// An [`IndexOracle`] that knows about no indexes.
368#[derive(Debug)]
369pub struct EmptyIndexOracle;
370
371impl IndexOracle for EmptyIndexOracle {
372 fn indexes_on(
373 &self,
374 _id: GlobalId,
375 ) -> Box<dyn Iterator<Item = (GlobalId, &[MirScalarExpr])> + '_> {
376 Box::new(iter::empty())
377 }
378}
379
380/// A trait for a type that can estimate statistics about a given `GlobalId`
381pub trait StatisticsOracle: fmt::Debug + Send {
382 /// Returns a cardinality estimate for the given identifier
383 ///
384 /// Returning `None` means "no estimate"; returning `Some(0)` means estimating that the shard backing `id` is empty
385 fn cardinality_estimate(&self, id: GlobalId) -> Option<usize>;
386
387 /// Returns a map from identifiers to sizes
388 fn as_map(&self) -> BTreeMap<GlobalId, usize>;
389}
390
391/// A [`StatisticsOracle`] that knows nothing and can give no estimates.
392#[derive(Debug)]
393pub struct EmptyStatisticsOracle;
394
395impl StatisticsOracle for EmptyStatisticsOracle {
396 fn cardinality_estimate(&self, _: GlobalId) -> Option<usize> {
397 None
398 }
399
400 fn as_map(&self) -> BTreeMap<GlobalId, usize> {
401 BTreeMap::new()
402 }
403}
404
405/// A sequence of transformations iterated some number of times.
406#[derive(Debug)]
407pub struct Fixpoint {
408 name: &'static str,
409 transforms: Vec<Box<dyn Transform>>,
410 limit: usize,
411}
412
413impl Fixpoint {
414 /// Run a single iteration of the [`Fixpoint`] transform by iterating
415 /// through all transforms.
416 #[mz_ore::instrument(
417 target = "optimizer",
418 level = "debug",
419 fields(path.segment = iter_name)
420 )]
421 fn apply_transforms(
422 &self,
423 relation: &mut MirRelationExpr,
424 ctx: &mut TransformCtx,
425 iter_name: String,
426 ) -> Result<(), TransformError> {
427 for transform in self.transforms.iter() {
428 transform.transform(relation, ctx)?;
429 }
430 mz_repr::explain::trace_plan(relation);
431 Ok(())
432 }
433}
434
435impl Transform for Fixpoint {
436 fn name(&self) -> &'static str {
437 self.name
438 }
439
440 #[mz_ore::instrument(
441 target = "optimizer",
442 level = "debug",
443 fields(path.segment = self.name)
444 )]
445 fn actually_perform_transform(
446 &self,
447 relation: &mut MirRelationExpr,
448 ctx: &mut TransformCtx,
449 ) -> Result<(), TransformError> {
450 // The number of iterations for a relation to settle depends on the
451 // number of nodes in the relation. Instead of picking an arbitrary
452 // hard limit on the number of iterations, we use a soft limit and
453 // check whether the relation has become simpler after reaching it.
454 // If so, we perform another pass of transforms. Otherwise, there is
455 // a bug somewhere that prevents the relation from settling on a
456 // stable shape.
457 let mut iter_no = 0;
458 let mut seen = BTreeMap::new();
459 seen.insert(relation.hash_to_u64(), iter_no);
460 let original = relation.clone();
461 loop {
462 let prev_size = relation.size();
463 for i in iter_no..iter_no + self.limit {
464 let prev = relation.clone();
465 self.apply_transforms(relation, ctx, format!("{i:04}"))?;
466 if *relation == prev {
467 if prev_size > 100000 {
468 tracing::warn!(%prev_size, "Very big MIR plan");
469 }
470 mz_repr::explain::trace_plan(relation);
471 return Ok(());
472 }
473 let seen_i = seen.insert(relation.hash_to_u64(), i);
474 if let Some(seen_i) = seen_i {
475 // Let's see whether this is just a hash collision, or a real loop: Run the
476 // whole thing from the beginning up until `seen_i`, and compare all the plans
477 // to the current plan from the outer `for`.
478 // (It would not be enough to compare only the plan at `seen_i`, because
479 // then we could miss a real loop if there is also a hash collision somewhere
480 // in the middle of the loop, because then we'd compare the last plan of the
481 // loop not with its actual match, but with the colliding plan.)
482 let mut again = original.clone();
483 // The `+2` is because:
484 // - one `+1` is to finally get to the plan at `seen_i`,
485 // - another `+1` is because we are comparing to `relation` only _before_
486 // calling `apply_transforms`.
487 for j in 0..(seen_i + 2) {
488 if again == *relation {
489 // We really got into an infinite loop (e.g., we are oscillating between
490 // two plans). This is not catastrophic, because we can just say we are
491 // done now, but it would be great to eventually find a way to prevent
492 // these loops from happening in the first place. We have several
493 // relevant issues, see
494 // https://github.com/MaterializeInc/database-issues/issues/8197#issuecomment-2200172227
495 mz_repr::explain::trace_plan(relation);
496 error!(
497 "Fixpoint `{}` detected a loop of length {} after {} iterations",
498 self.name,
499 i - seen_i,
500 i
501 );
502 return Ok(());
503 }
504 ctx.update_last_hash(&again);
505 self.apply_transforms(
506 &mut again,
507 ctx,
508 format!("collision detection {j:04}"),
509 )?;
510 }
511 // If we got here, then this was just a hash collision! Just continue as if
512 // nothing happened.
513 }
514 }
515 let current_size = relation.size();
516
517 iter_no += self.limit;
518
519 if current_size < prev_size {
520 tracing::warn!(
521 "Fixpoint {} ran for {} iterations \
522 without reaching a fixpoint but reduced the relation size; \
523 current_size ({}) < prev_size ({}); \
524 continuing for {} more iterations",
525 self.name,
526 iter_no,
527 current_size,
528 prev_size,
529 self.limit
530 );
531 } else {
532 // We failed to reach a fixed point, or find a sufficiently short cycle.
533 // This is not catastrophic, because we can just say we are done now,
534 // but it would be great to eventually find a way to prevent these loops from
535 // happening in the first place. We have several relevant issues, see
536 // https://github.com/MaterializeInc/database-issues/issues/8197#issuecomment-2200172227
537 mz_repr::explain::trace_plan(relation);
538 soft_panic_or_log!(
539 "Fixpoint {} failed to reach a fixed point, or cycle of length at most {}",
540 self.name,
541 self.limit,
542 );
543 return Ok(());
544 }
545 }
546 }
547}
548
549/// Convenience macro for guarding transforms behind a feature flag.
550///
551/// If you have a code block like
552///
553/// ```ignore
554/// vec![
555/// Box::new(Foo::default()),
556/// Box::new(Bar::default()),
557/// Box::new(Baz::default()),
558/// ]
559/// ```
560///
561/// and you want to guard `Bar` behind a feature flag `enable_bar`, you can
562/// write
563///
564/// ```ignore
565/// transforms![
566/// Box::new(Foo::default()),
567/// Box::new(Bar::default()); if ctx.features.enable_bar,
568/// Box::new(Baz::default()),
569/// ]
570/// ```
571///
572/// as a shorthand and in order to minimize your code diff.
573#[allow(unused_macros)]
574macro_rules! transforms {
575 // Internal rule. Matches lines with a guard: `$transform; if $cond`.
576 (@op fill $buf:ident with $transform:expr; if $cond:expr, $($transforms:tt)*) => {
577 if $cond {
578 $buf.push($transform);
579 }
580 transforms!(@op fill $buf with $($transforms)*);
581 };
582 // Internal rule. Matches lines without a guard: `$transform`.
583 (@op fill $buf:ident with $transform:expr, $($transforms:tt)*) => {
584 $buf.push($transform);
585 transforms!(@op fill $buf with $($transforms)*);
586 };
587 // Internal rule: matches the empty $transforms TokenTree (terminal case).
588 (@op fill $buf:ident with) => {
589 // do nothing
590 };
591 ($($transforms:tt)*) => {{
592 #[allow(clippy::vec_init_then_push)]
593 {
594 let mut __buf = Vec::<Box<dyn Transform>>::new();
595 transforms!(@op fill __buf with $($transforms)*);
596 __buf
597 }
598 }};
599}
600
601/// A sequence of transformations that simplify the `MirRelationExpr`
602#[derive(Debug)]
603pub struct FuseAndCollapse {
604 transforms: Vec<Box<dyn Transform>>,
605}
606
607impl Default for FuseAndCollapse {
608 fn default() -> Self {
609 Self {
610 // TODO: The relative orders of the transforms have not been
611 // determined except where there are comments.
612 // TODO (database-issues#2036): All the transforms here except for `ProjectionLifting`
613 // and `RedundantJoin` can be implemented as free functions.
614 transforms: vec![
615 Box::new(canonicalization::ProjectionExtraction),
616 Box::new(movement::ProjectionLifting::default()),
617 Box::new(fusion::Fusion),
618 Box::new(canonicalization::FlatMapElimination),
619 Box::new(fusion::join::Join),
620 Box::new(NormalizeLets::new(false)),
621 Box::new(fusion::reduce::Reduce),
622 Box::new(WillDistinct),
623 Box::new(compound::UnionNegateFusion),
624 // This goes after union fusion so we can cancel out
625 // more branches at a time.
626 Box::new(UnionBranchCancellation),
627 // This should run before redundant join to ensure that key info
628 // is correct.
629 Box::new(NormalizeLets::new(false)),
630 // Removes redundant inputs from joins.
631 // Note that this eliminates one redundant input per join,
632 // so it is necessary to run this section in a loop.
633 Box::new(RedundantJoin::default()),
634 // As a final logical action, convert any constant expression to a constant.
635 // Some optimizations fight against this, and we want to be sure to end as a
636 // `MirRelationExpr::Constant` if that is the case, so that subsequent use can
637 // clearly see this.
638 Box::new(fold_constants_fixpoint(true)),
639 ],
640 }
641 }
642}
643
644impl Transform for FuseAndCollapse {
645 fn name(&self) -> &'static str {
646 "FuseAndCollapse"
647 }
648
649 #[mz_ore::instrument(
650 target = "optimizer",
651 level = "debug",
652 fields(path.segment = "fuse_and_collapse")
653 )]
654 fn actually_perform_transform(
655 &self,
656 relation: &mut MirRelationExpr,
657 ctx: &mut TransformCtx,
658 ) -> Result<(), TransformError> {
659 for transform in self.transforms.iter() {
660 transform.transform(relation, ctx)?;
661 }
662 mz_repr::explain::trace_plan(&*relation);
663 Ok(())
664 }
665}
666
667/// Run the [`FuseAndCollapse`] transforms in a fixpoint.
668pub fn fuse_and_collapse_fixpoint() -> Fixpoint {
669 Fixpoint {
670 name: "fuse_and_collapse_fixpoint",
671 limit: 100,
672 transforms: FuseAndCollapse::default().transforms,
673 }
674}
675
676/// Does constant folding to a fixpoint: An expression all of whose leaves are constants, of size
677/// small enough to be inlined and folded should reach a single `MirRelationExpr::Constant`.
678///
679/// If `limit` is false, it does constant folding even on large constants.
680///
681/// This needs to call `FoldConstants` together with `NormalizeLets` in a fixpoint loop, because
682/// currently `FoldConstants` doesn't inline CTEs, so these two need to alternate until fixpoint.
683///
684/// Also note that `FoldConstants` can break the normalized form by removing all references to a
685/// Let.
686///
687/// We also call `ReduceScalars`, because that does constant folding inside scalar expressions.
688pub fn fold_constants_fixpoint(limit: bool) -> Fixpoint {
689 Fixpoint {
690 name: "fold_constants_fixpoint",
691 limit: 100,
692 transforms: vec![
693 Box::new(FoldConstants {
694 limit: if limit {
695 Some(FOLD_CONSTANTS_LIMIT)
696 } else {
697 None
698 },
699 }),
700 Box::new(canonicalization::ReduceScalars),
701 Box::new(NormalizeLets::new(false)),
702 ],
703 }
704}
705
706/// Construct a normalizing transform that runs transforms that normalize the
707/// structure of the tree until a fixpoint.
708///
709/// Care needs to be taken to ensure that the fixpoint converges for every
710/// possible input tree. If this is not the case, there are two possibilities:
711/// 1. The rewrite loop runs enters an oscillating cycle.
712/// 2. The expression grows without bound.
713pub fn normalize() -> Fixpoint {
714 Fixpoint {
715 name: "normalize",
716 limit: 100,
717 transforms: vec![Box::new(NormalizeLets::new(false)), Box::new(NormalizeOps)],
718 }
719}
720
721/// A naive optimizer for relation expressions.
722///
723/// The optimizer currently applies only peep-hole optimizations, from a limited
724/// set that were sufficient to get some of TPC-H up and working. It is worth a
725/// review at some point to improve the quality, coverage, and architecture of
726/// the optimizations.
727#[derive(Debug)]
728pub struct Optimizer {
729 /// A logical name identifying this optimizer instance.
730 pub name: &'static str,
731 /// The list of transforms to apply to an input relation.
732 pub transforms: Vec<Box<dyn Transform>>,
733}
734
735impl Optimizer {
736 /// Builds a logical optimizer that only performs logical transformations.
737 #[deprecated = "Create an Optimize instance and call `optimize` instead."]
738 pub fn logical_optimizer(ctx: &mut TransformCtx) -> Self {
739 let transforms: Vec<Box<dyn Transform>> = transforms![
740 // 0. `Transform`s that don't actually change the plan.
741 Box::new(Typecheck::new(ctx.typechecking_context()).strict_join_equivalences()),
742 Box::new(CollectNotices),
743 // 1. Structure-agnostic cleanup
744 Box::new(normalize()),
745 Box::new(NonNullRequirements::default()),
746 // 2. Collapse constants, joins, unions, and lets as much as possible.
747 // TODO: lift filters/maps to maximize ability to collapse
748 // things down?
749 Box::new(fuse_and_collapse_fixpoint()),
750 // 3. Needs to happen before LiteralLifting, EquivalencePropagation
751 // make (literal) filters look more complicated than what the NonNegative Analysis can
752 // recognize.
753 Box::new(ThresholdElision),
754 // 4. Move predicate information up and down the tree.
755 // This also fixes the shape of joins in the plan.
756 Box::new(Fixpoint {
757 name: "fixpoint_logical_01",
758 limit: 100,
759 transforms: vec![
760 // Predicate pushdown sets the equivalence classes of joins.
761 Box::new(PredicatePushdown::default()),
762 Box::new(EquivalencePropagation::default()),
763 // Lifts the information `col1 = col2`
764 Box::new(Demand::default()),
765 Box::new(FuseAndCollapse::default()),
766 ],
767 }),
768 // 5. Reduce/Join simplifications.
769 Box::new(Fixpoint {
770 name: "fixpoint_logical_02",
771 limit: 100,
772 transforms: vec![
773 Box::new(SemijoinIdempotence::default()),
774 // Pushes aggregations down
775 Box::new(ReductionPushdown),
776 // Replaces reduces with maps when the group keys are
777 // unique with maps
778 Box::new(ReduceElision),
779 // Rips complex reduces apart.
780 Box::new(ReduceReduction),
781 // Converts `Cross Join {Constant(Literal) + Input}` to
782 // `Map {Cross Join (Input, Constant()), Literal}`.
783 // Join fusion will clean this up to `Map{Input, Literal}`
784 Box::new(LiteralLifting::default()),
785 // Identifies common relation subexpressions.
786 Box::new(cse::relation_cse::RelationCSE::new(false)),
787 Box::new(FuseAndCollapse::default()),
788 ],
789 }),
790 Box::new(
791 Typecheck::new(ctx.typechecking_context())
792 .disallow_new_globals()
793 .strict_join_equivalences()
794 ),
795 ];
796 Self {
797 name: "logical",
798 transforms,
799 }
800 }
801
802 /// Builds a physical optimizer.
803 ///
804 /// Performs logical transformations followed by all physical ones.
805 /// This is meant to be used for optimizing each view within a dataflow
806 /// once view inlining has already happened, right before dataflow
807 /// rendering.
808 pub fn physical_optimizer(ctx: &mut TransformCtx) -> Self {
809 // Implementation transformations
810 let transforms: Vec<Box<dyn Transform>> = transforms![
811 Box::new(
812 Typecheck::new(ctx.typechecking_context())
813 .disallow_new_globals()
814 .strict_join_equivalences(),
815 ),
816 // Considerations for the relationship between JoinImplementation and other transforms:
817 // - there should be a run of LiteralConstraints before JoinImplementation lifts away
818 // the Filters from the Gets;
819 // - there should be no RelationCSE between this LiteralConstraints and
820 // JoinImplementation, because that could move an IndexedFilter behind a Get.
821 // - The last RelationCSE before JoinImplementation should be with inline_mfp = true.
822 // - Currently, JoinImplementation can't be before LiteralLifting because the latter
823 // sometimes creates `Unimplemented` joins (despite LiteralLifting already having been
824 // run in the logical optimizer).
825 // - Not running EquivalencePropagation in the same fixpoint loop with JoinImplementation
826 // is slightly hurting our plans. However, I'd say we should fix these problems by
827 // making EquivalencePropagation (and/or JoinImplementation) smarter (database-issues#5289), rather than
828 // having them in the same fixpoint loop. If they would be in the same fixpoint loop,
829 // then we either run the risk of EquivalencePropagation invalidating a join plan (database-issues#5260),
830 // or we would have to run JoinImplementation an unbounded number of times, which is
831 // also not good database-issues#4639.
832 // (The same is true for FoldConstants, Demand, and LiteralLifting to a lesser
833 // extent.)
834 //
835 // Also note that FoldConstants and LiteralLifting are not confluent. They can
836 // oscillate between e.g.:
837 // Constant
838 // - (4)
839 // and
840 // Map (4)
841 // Constant
842 // - ()
843 Box::new(Fixpoint {
844 name: "fixpoint_physical_01",
845 limit: 100,
846 transforms: transforms![
847 Box::new(EquivalencePropagation::default()),
848 Box::new(fold_constants_fixpoint(true)),
849 Box::new(coalesce_case::CoalesceCase::default());
850 if ctx.features.enable_coalesce_case_transform,
851 Box::new(Demand::default()),
852 // Demand might have introduced dummies, so let's also do a ProjectionPushdown.
853 Box::new(ProjectionPushdown::default()),
854 Box::new(LiteralLifting::default()),
855 ],
856 }),
857 Box::new(LiteralConstraints),
858 Box::new(Fixpoint {
859 name: "fixpoint_join_impl",
860 limit: 100,
861 transforms: vec![Box::new(JoinImplementation::default())],
862 }),
863 Box::new(CanonicalizeMfp),
864 // Identifies common relation subexpressions.
865 Box::new(cse::relation_cse::RelationCSE::new(false)),
866 // `RelationCSE` can create new points of interest for `ProjectionPushdown`: If an MFP
867 // is cut in half by `RelationCSE`, then we'd like to push projections behind the new
868 // Get as much as possible. This is because a fork in the plan involves copying the
869 // data. (But we need `ProjectionPushdown` to skip joins, because it can't deal with
870 // filled in JoinImplementations.)
871 Box::new(ProjectionPushdown::skip_joins());
872 if ctx.features.enable_projection_pushdown_after_relation_cse,
873 // Plans look nicer if we tidy MFPs again after ProjectionPushdown.
874 Box::new(CanonicalizeMfp);
875 if ctx.features.enable_projection_pushdown_after_relation_cse,
876 // Rewrite If-chains matching a single expression against literals
877 // into a CaseLiteral lookup for O(log n) evaluation.
878 Box::new(case_literal::CaseLiteralTransform);
879 if ctx.features.enable_case_literal_transform,
880 // Do a last run of constant folding. Importantly, this also runs `NormalizeLets`!
881 // We need `NormalizeLets` at the end of the MIR pipeline for various reasons:
882 // - The rendering expects some invariants about Let/LetRecs.
883 // - `CollectIndexRequests` needs a normalized plan.
884 // https://github.com/MaterializeInc/database-issues/issues/6371
885 Box::new(fold_constants_fixpoint(true)),
886 Box::new(
887 Typecheck::new(ctx.typechecking_context())
888 .disallow_new_globals()
889 .disallow_dummy()
890 .strict_join_equivalences(),
891 ),
892 ];
893 Self {
894 name: "physical",
895 transforms,
896 }
897 }
898
899 /// Contains the logical optimizations that should run after cross-view
900 /// transformations run.
901 ///
902 /// Set `allow_new_globals` when you will use these as the first passes.
903 /// The first instance of the typechecker in an optimizer pipeline should
904 /// allow new globals (or it will crash when it encounters them).
905 pub fn logical_cleanup_pass(ctx: &mut TransformCtx, allow_new_globals: bool) -> Self {
906 let mut repr_typechecker =
907 Typecheck::new(ctx.typechecking_context()).strict_join_equivalences();
908 if !allow_new_globals {
909 repr_typechecker = repr_typechecker.disallow_new_globals();
910 }
911
912 let transforms: Vec<Box<dyn Transform>> = transforms![
913 Box::new(repr_typechecker),
914 // Delete unnecessary maps.
915 Box::new(fusion::Fusion),
916 Box::new(Fixpoint {
917 name: "fixpoint_logical_cleanup_pass_01",
918 limit: 100,
919 transforms: vec![
920 Box::new(CanonicalizeMfp),
921 // Remove threshold operators which have no effect.
922 Box::new(ThresholdElision),
923 // Projection pushdown may unblock fusing joins and unions.
924 Box::new(fusion::join::Join),
925 // Predicate pushdown required to tidy after join fusion.
926 Box::new(PredicatePushdown::default()),
927 Box::new(RedundantJoin::default()),
928 // Redundant join produces projects that need to be fused.
929 Box::new(fusion::Fusion),
930 Box::new(compound::UnionNegateFusion),
931 // This goes after union fusion so we can cancel out
932 // more branches at a time.
933 Box::new(UnionBranchCancellation),
934 // The last RelationCSE before JoinImplementation should be with
935 // inline_mfp = true.
936 Box::new(cse::relation_cse::RelationCSE::new(true)),
937 Box::new(fold_constants_fixpoint(true)),
938 ],
939 }),
940 Box::new(
941 Typecheck::new(ctx.typechecking_context())
942 .disallow_new_globals()
943 .strict_join_equivalences()
944 ),
945 ];
946 Self {
947 name: "logical_cleanup",
948 transforms,
949 }
950 }
951
952 /// Builds a tiny optimizer, which is only suitable for optimizing fast-path queries.
953 pub fn fast_path_optimizer(_ctx: &mut TransformCtx) -> Self {
954 let transforms: Vec<Box<dyn Transform>> = vec![
955 Box::new(canonicalization::ReduceScalars),
956 Box::new(LiteralConstraints),
957 Box::new(CanonicalizeMfp),
958 // We might have arrived at a constant, e.g., due to contradicting literal constraints.
959 Box::new(Fixpoint {
960 name: "fast_path_fold_constants_fixpoint",
961 limit: 100,
962 transforms: vec![
963 Box::new(FoldConstants {
964 limit: Some(FOLD_CONSTANTS_LIMIT),
965 }),
966 Box::new(canonicalization::ReduceScalars),
967 ],
968 }),
969 ];
970 Self {
971 name: "fast_path_optimizer",
972 transforms,
973 }
974 }
975
976 /// Builds a tiny optimizer, which just folds constants. For more details, see
977 /// [fold_constants_fixpoint].
978 pub fn constant_optimizer(_ctx: &mut TransformCtx, limit: bool) -> Self {
979 Self {
980 name: "fast_path_optimizer",
981 transforms: vec![Box::new(fold_constants_fixpoint(limit))],
982 }
983 }
984
985 /// Optimizes the supplied relation expression.
986 ///
987 /// These optimizations are performed with no information about available arrangements,
988 /// which makes them suitable for pre-optimization before dataflow deployment.
989 #[mz_ore::instrument(
990 target = "optimizer",
991 level = "debug",
992 fields(path.segment = self.name)
993 )]
994 pub fn optimize(
995 &self,
996 mut relation: MirRelationExpr,
997 ctx: &mut TransformCtx,
998 ) -> Result<mz_expr::OptimizedMirRelationExpr, TransformError> {
999 let transform_result = self.transform(&mut relation, ctx);
1000
1001 match transform_result {
1002 Ok(_) => {
1003 mz_repr::explain::trace_plan(&relation);
1004 Ok(mz_expr::OptimizedMirRelationExpr(relation))
1005 }
1006 Err(e) => {
1007 // Without this, the dropping of `relation` (which happens automatically when
1008 // returning from this function) might run into a stack overflow, see
1009 // https://github.com/MaterializeInc/database-issues/issues/4043
1010 relation.destroy_carefully();
1011 error!("Optimizer::optimize(): {}", e);
1012 Err(e)
1013 }
1014 }
1015 }
1016
1017 /// Optimizes the supplied relation expression in place, using available arrangements.
1018 ///
1019 /// This method should only be called with non-empty `indexes` when optimizing a dataflow,
1020 /// as the optimizations may lock in the use of arrangements that may cease to exist.
1021 fn transform(
1022 &self,
1023 relation: &mut MirRelationExpr,
1024 args: &mut TransformCtx,
1025 ) -> Result<(), TransformError> {
1026 args.update_last_hash(relation);
1027
1028 for transform in self.transforms.iter() {
1029 transform.transform(relation, args)?;
1030 }
1031
1032 Ok(())
1033 }
1034}