Skip to main content

mz_adapter/
optimize.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//! Optimizer interface to the adapter and coordinator code.
11//!
12//! The goal of this crate is to abstract optimizer specifics behind a
13//! high-level interface that is ready to be consumed by the coordinator code in
14//! a future-proof way (that is, the API is taking the upcoming evolution of
15//! these components into account).
16//!
17//! The contents of this crate should have minimal dependencies to the rest of
18//! the coordinator code so we can pull them out as a separate crate in the
19//! future without too much effort.
20//!
21//! The main type in this module is a very simple [`Optimize`] trait which
22//! allows us to adhere to the following principles:
23//!
24//! - Implementors of this trait are structs that encapsulate all context
25//!   required to optimize a statement of type `T` end-to-end (for example
26//!   [`materialized_view::Optimizer`] for `T` = `MaterializedView`).
27//! - Each struct implements [`Optimize`] once for each optimization stage. The
28//!   `From` type represents the input of the stage and `Self::To` the
29//!   associated stage output. This allows to have more than one entrypoints to
30//!   a pipeline.
31//! - The concrete types used for stage results are opaque structs that are
32//!   specific to the pipeline of that statement type.
33//!   - We use different structs even if two statement types might have
34//!     structurally identical intermediate results. This ensures that client
35//!     code cannot first execute some optimization stages for one type and then
36//!     some stages for a different type.
37//!   - The only way to construct such a struct is by running the [`Optimize`]
38//!     stage that produces it. This ensures that client code cannot interfere
39//!     with the pipeline.
40//!   - In general, the internals of these structs can be accessed only behind a
41//!     shared reference. This ensures that client code can look up information
42//!     from intermediate stages but cannot modify it.
43//!   - Timestamp selection is modeled as a conversion between structs that are
44//!     adjacent in the pipeline using a method called `resolve`.
45//!   - The struct representing the result of the final stage of the
46//!     optimization pipeline can be destructed to access its internals with a
47//!     method called `unapply`.
48//! - The `Send` trait bounds on the `Self` and `From` types ensure that
49//!   [`Optimize`] instances can be passed to different threads (this is
50//!   required of off-thread optimization).
51//!
52//! For details, see the `20230714_optimizer_interface.md` design doc in this
53//! repository.
54
55pub mod copy_to;
56pub mod dataflows;
57pub mod index;
58pub mod materialized_view;
59mod metric_sink;
60pub mod peek;
61pub mod subscribe;
62pub mod view;
63
64use std::fmt::Debug;
65
66use mz_adapter_types::connection::ConnectionId;
67use mz_adapter_types::dyncfgs::PERSIST_FAST_PATH_ORDER;
68use mz_catalog::memory::objects::{CatalogCollectionEntry, CatalogEntry, Index};
69use mz_compute_types::ComputeInstanceId;
70use mz_compute_types::dataflows::DataflowDescription;
71use mz_compute_types::dyncfgs::SUBSCRIBE_SNAPSHOT_OPTIMIZATION;
72use mz_compute_types::plan::LirRelationExpr;
73use mz_controller_types::ClusterId;
74use mz_expr::{EvalError, MirRelationExpr, OptimizedMirRelationExpr, UnmaterializableFunc};
75use mz_ore::stack::RecursionLimitError;
76use mz_repr::adt::timestamp::TimestampError;
77use mz_repr::optimize::{OptimizerFeatureOverrides, OptimizerFeatures, OverrideFrom};
78use mz_repr::{CatalogItemId, GlobalId};
79use mz_sql::names::{FullItemName, QualifiedItemName};
80use mz_sql::plan::{HirRelationExpr, PlanError};
81use mz_sql::session::metadata::SessionMetadata;
82use mz_sql::session::vars::SystemVars;
83use mz_transform::{MaybeShouldPanic, StatisticsOracle, TransformCtx, TransformError};
84
85use crate::TimestampContext;
86
87// Alias types
88// -----------
89
90/// A type for a [`DataflowDescription`] backed by `Mir~` plans. Used internally
91/// by the optimizer implementations.
92type MirDataflowDescription = DataflowDescription<OptimizedMirRelationExpr>;
93/// A type for a [`DataflowDescription`] backed by `Lir~` plans. Used internally
94/// by the optimizer implementations.
95type LirDataflowDescription = DataflowDescription<LirRelationExpr>;
96
97// Core API
98// --------
99
100/// A trait that represents an optimization stage.
101///
102/// The trait is implemented by structs that encapsulate the context needed to
103/// run an end-to-end optimization pipeline for a specific statement type
104/// (`Index`, `View`, `MaterializedView`, `Subscribe`, `Select`).
105///
106/// Each implementation represents a concrete optimization stage for a fixed
107/// statement type that consumes an input of type `From` and produces output of
108/// type `Self::To`.
109pub trait Optimize<From> {
110    type To;
111
112    /// Execute the optimization stage, transforming the input plan of type
113    /// `From` to an output plan of type `To`.
114    fn optimize(&mut self, plan: From) -> Result<Self::To, OptimizerError>;
115
116    /// Like [`Self::optimize`], but additionally ensures that panics occurring
117    /// in the [`Self::optimize`] call are caught and demoted to an
118    /// [`OptimizerError::Internal`] error.
119    ///
120    /// Additionally, if the result of the optimization is an error (not a panic) that indicates we
121    /// should panic, then panic.
122    #[mz_ore::instrument(target = "optimizer", level = "debug", name = "optimize")]
123    fn catch_unwind_optimize(&mut self, plan: From) -> Result<Self::To, OptimizerError> {
124        mz_transform::catch_unwind_optimize(|| self.optimize(plan))
125    }
126}
127
128// One-shot peek optimizer dispatch
129// --------------------------------
130
131/// The optimizer driving a one-shot statement through the peek sequencing state
132/// machine.
133///
134/// `SELECT` and `EXPLAIN` are optimized by the [`peek::Optimizer`], while `COPY
135/// TO` is optimized by the [`copy_to::Optimizer`]. Both share the same
136/// surrounding state machine (timestamp selection, read holds, off-thread
137/// optimization, …), so this enum lets that shared machinery carry either
138/// optimizer without caring which one it is. The variants are kept distinct
139/// (rather than abstracted behind a trait object) because the downstream stages
140/// need to recover the concrete optimizer and its statement-specific result.
141#[derive(Debug)]
142pub enum PeekOptimizer {
143    /// Optimizer for `SELECT` and `EXPLAIN` statements.
144    Select(peek::Optimizer),
145    /// Optimizer for `COPY TO` statements.
146    CopyTo(copy_to::Optimizer),
147}
148
149/// The global LIR plan produced by [`PeekOptimizer::optimize`], tagged with the
150/// path that produced it.
151#[derive(Debug)]
152pub enum PeekGlobalLirPlan {
153    /// The result of the `SELECT`/`EXPLAIN` pipeline.
154    Select(peek::GlobalLirPlan),
155    /// The result of the `COPY TO` pipeline.
156    CopyTo(copy_to::GlobalLirPlan),
157}
158
159impl PeekOptimizer {
160    /// The cluster that will run the optimized dataflow.
161    pub fn cluster_id(&self) -> ComputeInstanceId {
162        match self {
163            PeekOptimizer::Select(optimizer) => optimizer.cluster_id(),
164            PeekOptimizer::CopyTo(optimizer) => optimizer.cluster_id(),
165        }
166    }
167
168    /// Runs the full one-shot optimization pipeline end-to-end:
169    ///
170    /// 1. HIR ⇒ MIR lowering and local MIR optimization,
171    /// 2. timestamp resolution,
172    /// 3. global MIR optimization, MIR ⇒ LIR lowering, and global LIR
173    ///    optimization.
174    ///
175    /// The pipeline shape is identical for both variants; only the concrete
176    /// (statement-specific) plan types differ, so the steps are shared via
177    /// [`optimize_oneshot`].
178    pub fn optimize(
179        &mut self,
180        raw_expr: HirRelationExpr,
181        timestamp_ctx: TimestampContext,
182        session: &dyn SessionMetadata,
183        stats: Box<dyn StatisticsOracle>,
184    ) -> Result<PeekGlobalLirPlan, OptimizerError> {
185        match self {
186            PeekOptimizer::Select(optimizer) => {
187                let plan = optimize_oneshot(optimizer, raw_expr, |local_mir_plan| {
188                    local_mir_plan.resolve(timestamp_ctx, session, stats)
189                })?;
190                Ok(PeekGlobalLirPlan::Select(plan))
191            }
192            PeekOptimizer::CopyTo(optimizer) => {
193                let plan = optimize_oneshot(optimizer, raw_expr, |local_mir_plan| {
194                    local_mir_plan.resolve(timestamp_ctx, session, stats)
195                })?;
196                Ok(PeekGlobalLirPlan::CopyTo(plan))
197            }
198        }
199    }
200
201    /// Consumes `self`, returning the inner [`peek::Optimizer`] if this is the
202    /// `SELECT`/`EXPLAIN` path and `None` otherwise.
203    pub fn into_select(self) -> Option<peek::Optimizer> {
204        match self {
205            PeekOptimizer::Select(optimizer) => Some(optimizer),
206            PeekOptimizer::CopyTo(_) => None,
207        }
208    }
209
210    /// Consumes `self`, returning the inner [`copy_to::Optimizer`] if this is
211    /// the `COPY TO` path and `None` otherwise.
212    pub fn into_copy_to(self) -> Option<copy_to::Optimizer> {
213        match self {
214            PeekOptimizer::CopyTo(optimizer) => Some(optimizer),
215            PeekOptimizer::Select(_) => None,
216        }
217    }
218}
219
220/// Runs the shared one-shot optimization pipeline for a single optimizer.
221///
222/// This factors out the (otherwise duplicated) HIR ⇒ local MIR ⇒ resolve ⇒
223/// global LIR sequence that is common to the `SELECT`/`EXPLAIN` and `COPY TO`
224/// paths. The `resolve` closure attaches the timestamp/session/stats context to
225/// the local plan; it is path-specific only in the concrete plan type it
226/// operates on.
227pub(crate) fn optimize_oneshot<O, LocalPlan, ResolvedPlan, GlobalPlan>(
228    optimizer: &mut O,
229    raw_expr: HirRelationExpr,
230    resolve: impl FnOnce(LocalPlan) -> ResolvedPlan,
231) -> Result<GlobalPlan, OptimizerError>
232where
233    O: Optimize<HirRelationExpr, To = LocalPlan> + Optimize<ResolvedPlan, To = GlobalPlan>,
234{
235    // HIR ⇒ MIR lowering and MIR optimization (local).
236    let local_mir_plan = optimizer.catch_unwind_optimize(raw_expr)?;
237    // Attach resolved context required to continue the pipeline.
238    let resolved_mir_plan = resolve(local_mir_plan);
239    // MIR optimization (global), MIR ⇒ LIR lowering, and LIR optimization (global).
240    optimizer.catch_unwind_optimize(resolved_mir_plan)
241}
242
243// Optimizer configuration
244// -----------------------
245
246/// Feature flags for the optimizer.
247///
248/// To add a new feature flag, do the following steps:
249///
250/// 1. To make the flag available to all stages in our [`Optimize`] pipelines
251///    and allow engineers to set a system-wide override:
252///    1. Add the flag to the `optimizer_feature_flags!(...)` macro call.
253///    2. Add the flag to the `feature_flags!(...)` macro call and extend the
254///       `From<&SystemVars>` implementation for [`OptimizerFeatures`].
255///
256/// 2. To enable `EXPLAIN ... WITH(...)` overrides which will allow engineers to
257///    inspect plan differences before deploying the optimizer changes:
258///    1. Add the flag to the `ExplainPlanOptionName` definition.
259///    2. Add the flag to the `generate_extracted_config!(ExplainPlanOption,
260///       ...)` macro call.
261///    3. Extend the `TryFrom<ExplainPlanOptionExtracted>` implementation for
262///       [`mz_repr::explain::ExplainConfig`].
263///
264/// 3. To enable `CLUSTER ... FEATURES(...)` overrides which will allow
265///    engineers to experiment with runtime differences before deploying the
266///    optimizer changes:
267///    1. Add the flag to the `ClusterFeatureName` definition.
268///    2. Add the flag to the `generate_extracted_config!(ClusterFeature, ...)`
269///       macro call.
270///    3. Extend the `let optimizer_feature_overrides = ...` call in
271///       `plan_create_cluster`.
272#[derive(Clone, Debug)]
273pub struct OptimizerConfig {
274    /// The mode in which the optimizer runs.
275    pub mode: OptimizeMode,
276    /// If the [`GlobalId`] is set the optimizer works in "replan" mode.
277    ///
278    /// This means that it will not consider catalog items (more specifically
279    /// indexes) with [`GlobalId`] greater or equal than the one provided here.
280    pub replan: Option<GlobalId>,
281    /// Show the slow path plan even if a fast path plan was created. Useful for debugging.
282    /// Enforced if `timing` is set.
283    pub no_fast_path: bool,
284    // If set, allow some additional queries down the Persist fast path when we believe
285    // the orderings are compatible.
286    persist_fast_path_order: bool,
287    // Enable calculating with_snapshot metadata for subscribes.
288    subscribe_snapshot_optimization: bool,
289    /// Optimizer feature flags.
290    pub features: OptimizerFeatures,
291}
292
293#[derive(Clone, Debug, PartialEq, Eq)]
294pub enum OptimizeMode {
295    /// A mode where the optimized statement is executed.
296    Execute,
297    /// A mode where the optimized statement is explained.
298    Explain,
299}
300
301impl From<&SystemVars> for OptimizerConfig {
302    fn from(vars: &SystemVars) -> Self {
303        Self {
304            mode: OptimizeMode::Execute,
305            replan: None,
306            no_fast_path: false,
307            persist_fast_path_order: PERSIST_FAST_PATH_ORDER.get(vars.dyncfgs()),
308            subscribe_snapshot_optimization: SUBSCRIBE_SNAPSHOT_OPTIMIZATION.get(vars.dyncfgs()),
309            features: OptimizerFeatures::from(vars),
310        }
311    }
312}
313
314/// Override [`OptimizerConfig::features`] from [`OptimizerFeatureOverrides`].
315impl OverrideFrom<OptimizerFeatureOverrides> for OptimizerConfig {
316    fn override_from(mut self, overrides: &OptimizerFeatureOverrides) -> Self {
317        self.features = self.features.override_from(overrides);
318        self
319    }
320}
321
322/// [`OptimizerConfig`] overrides coming from an [`ExplainContext`].
323impl OverrideFrom<ExplainContext> for OptimizerConfig {
324    fn override_from(mut self, ctx: &ExplainContext) -> Self {
325        let ExplainContext::Plan(ctx) = ctx else {
326            return self; // Return immediately for all other contexts.
327        };
328
329        // Override general parameters.
330        self.mode = OptimizeMode::Explain;
331        self.replan = ctx.replan;
332        self.no_fast_path = ctx.config.no_fast_path;
333
334        // Override feature flags that can be enabled in the EXPLAIN config.
335        self.features = self.features.override_from(&ctx.config.features);
336
337        // Return the final result.
338        self
339    }
340}
341
342impl From<&OptimizerConfig> for mz_sql::plan::HirToMirConfig {
343    fn from(config: &OptimizerConfig) -> Self {
344        Self {
345            enable_new_outer_join_lowering: config.features.enable_new_outer_join_lowering,
346            enable_variadic_left_join_lowering: config.features.enable_variadic_left_join_lowering,
347            enable_cast_elimination: config.features.enable_cast_elimination,
348            enable_simplify_quantified_comparisons: config
349                .features
350                .enable_simplify_quantified_comparisons,
351            enable_fixed_correlated_cte_lowering: config
352                .features
353                .enable_fixed_correlated_cte_lowering,
354            enable_simplify_from_less_existence: config
355                .features
356                .enable_simplify_from_less_existence,
357        }
358    }
359}
360
361// OptimizerCatalog
362// ===============
363
364pub trait OptimizerCatalog: Debug + Send + Sync {
365    fn get_entry(&self, id: &GlobalId) -> CatalogCollectionEntry;
366    fn get_entry_by_item_id(&self, id: &CatalogItemId) -> &CatalogEntry;
367    fn resolve_full_name(
368        &self,
369        name: &QualifiedItemName,
370        conn_id: Option<&ConnectionId>,
371    ) -> FullItemName;
372
373    /// Returns all indexes on the given object and cluster known in the
374    /// catalog.
375    fn get_indexes_on(
376        &self,
377        id: GlobalId,
378        cluster: ClusterId,
379    ) -> Box<dyn Iterator<Item = (GlobalId, &Index)> + '_>;
380}
381
382// OptimizerError
383// ===============
384
385/// Error types that can be generated during optimization.
386#[derive(Debug, thiserror::Error)]
387pub enum OptimizerError {
388    #[error("{0}")]
389    PlanError(#[from] PlanError),
390    #[error("{0}")]
391    RecursionLimitError(#[from] RecursionLimitError),
392    #[error("{0}")]
393    TransformError(#[from] TransformError),
394    #[error("{0}")]
395    EvalError(#[from] EvalError),
396    #[error("cannot materialize call to {0}")]
397    UnmaterializableFunction(UnmaterializableFunc),
398    #[error("cannot call {func} in {context} ")]
399    UncallableFunction {
400        func: UnmaterializableFunc,
401        context: &'static str,
402    },
403    #[error("access to function {0} is restricted")]
404    RestrictedFunction(UnmaterializableFunc),
405    #[error("{0}")]
406    UnsupportedTemporalExpression(String),
407    /// This is a specific kind of internal error. It's distinct from `Internal`, because we want to
408    /// catch it and swallow it in some cases.
409    #[error("internal optimizer error: MfpPlan couldn't be converted into SafeMfpPlan")]
410    InternalUnsafeMfpPlan(String),
411    #[error("internal optimizer error: {0}")]
412    Internal(String),
413}
414
415impl From<String> for OptimizerError {
416    fn from(msg: String) -> Self {
417        Self::Internal(msg)
418    }
419}
420
421impl OptimizerError {
422    pub fn detail(&self) -> Option<String> {
423        match self {
424            Self::UnmaterializableFunction(UnmaterializableFunc::CurrentTimestamp) => {
425                Some("See: https://materialize.com/docs/sql/functions/now_and_mz_now/".into())
426            }
427            Self::RestrictedFunction(_) => Some(
428                "Access to system catalog objects is restricted for this role. \
429                Contact your administrator if you need access."
430                    .into(),
431            ),
432            _ => None,
433        }
434    }
435
436    pub fn hint(&self) -> Option<String> {
437        match self {
438            Self::UnmaterializableFunction(UnmaterializableFunc::CurrentTimestamp) => {
439                Some("In temporal filters `mz_now()` may work instead.".into())
440            }
441            _ => None,
442        }
443    }
444}
445
446impl From<TimestampError> for OptimizerError {
447    fn from(value: TimestampError) -> Self {
448        OptimizerError::EvalError(EvalError::from(value))
449    }
450}
451
452impl From<anyhow::Error> for OptimizerError {
453    fn from(value: anyhow::Error) -> Self {
454        OptimizerError::Internal(value.to_string())
455    }
456}
457
458impl MaybeShouldPanic for OptimizerError {
459    fn should_panic(&self) -> Option<String> {
460        match self {
461            OptimizerError::TransformError(TransformError::CallerShouldPanic(msg)) => {
462                Some(msg.to_string())
463            }
464            _ => None,
465        }
466    }
467}
468
469// Tracing helpers
470// ---------------
471
472#[mz_ore::instrument(target = "optimizer", level = "debug", name = "local")]
473fn optimize_mir_local(
474    expr: MirRelationExpr,
475    ctx: &mut TransformCtx,
476) -> Result<OptimizedMirRelationExpr, OptimizerError> {
477    #[allow(deprecated)]
478    let optimizer = mz_transform::Optimizer::logical_optimizer(ctx);
479    let expr = optimizer.optimize(expr, ctx)?;
480
481    // Trace the result of this phase.
482    mz_repr::explain::trace_plan(expr.as_inner());
483
484    Ok::<_, OptimizerError>(expr)
485}
486
487/// This is just a wrapper around [mz_transform::Optimizer::constant_optimizer],
488/// running it, and tracing the result plan.
489#[mz_ore::instrument(target = "optimizer", level = "debug", name = "constant")]
490fn optimize_mir_constant(
491    expr: MirRelationExpr,
492    ctx: &mut TransformCtx,
493    limit: bool,
494) -> Result<MirRelationExpr, OptimizerError> {
495    let optimizer = mz_transform::Optimizer::constant_optimizer(ctx, limit);
496    let expr = optimizer.optimize(expr, ctx)?;
497
498    // Trace the result of this phase.
499    mz_repr::explain::trace_plan(expr.as_inner());
500
501    Ok::<_, OptimizerError>(expr.0)
502}
503
504macro_rules! trace_plan {
505    (at: $span:literal, $plan:expr) => {
506        tracing::debug_span!(target: "optimizer", $span).in_scope(|| {
507            mz_repr::explain::trace_plan($plan);
508        });
509    }
510}
511
512use trace_plan;
513
514use crate::coord::ExplainContext;