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