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