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;
59pub mod 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::dataflows::MirDataflowDescription;
70use mz_compute_types::dyncfgs::SUBSCRIBE_SNAPSHOT_OPTIMIZATION;
71use mz_controller_types::ClusterId;
72use mz_expr::{EvalError, MirRelationExpr, OptimizedMirRelationExpr, UnmaterializableFunc};
73use mz_ore::stack::RecursionLimitError;
74use mz_repr::adt::timestamp::TimestampError;
75use mz_repr::optimize::{OptimizerFeatureOverrides, OptimizerFeatures, OverrideFrom};
76use mz_repr::{CatalogItemId, GlobalId};
77use mz_sql::names::{FullItemName, QualifiedItemName};
78use mz_sql::plan::{HirRelationExpr, PlanError};
79use mz_sql::session::vars::SystemVars;
80use mz_transform::{MaybeShouldPanic, TransformCtx, TransformError};
81
82// Alias types
83// -----------
84
85pub use mz_compute_types::dataflows::LirDataflowDescription;
86
87// Core API
88// --------
89
90/// A trait that represents an optimization stage.
91///
92/// The trait is implemented by structs that encapsulate the context needed to
93/// run an end-to-end optimization pipeline for a specific statement type
94/// (`Index`, `View`, `MaterializedView`, `Subscribe`, `Select`).
95///
96/// Each implementation represents a concrete optimization stage for a fixed
97/// statement type that consumes an input of type `From` and produces output of
98/// type `Self::To`.
99pub trait Optimize<From> {
100 type To;
101
102 /// Execute the optimization stage, transforming the input plan of type
103 /// `From` to an output plan of type `To`.
104 fn optimize(&mut self, plan: From) -> Result<Self::To, OptimizerError>;
105
106 /// Like [`Self::optimize`], but additionally ensures that panics occurring
107 /// in the [`Self::optimize`] call are caught and demoted to an
108 /// [`OptimizerError::Internal`] error.
109 ///
110 /// Additionally, if the result of the optimization is an error (not a panic) that indicates we
111 /// should panic, then panic.
112 #[mz_ore::instrument(target = "optimizer", level = "debug", name = "optimize")]
113 fn catch_unwind_optimize(&mut self, plan: From) -> Result<Self::To, OptimizerError> {
114 mz_transform::catch_unwind_optimize(|| self.optimize(plan))
115 }
116}
117
118/// Runs the shared one-shot optimization pipeline for a single optimizer.
119///
120/// This factors out the (otherwise duplicated) HIR ⇒ local MIR ⇒ resolve ⇒
121/// global LIR sequence that is common to the `SELECT`/`EXPLAIN` and `COPY TO`
122/// paths. The `resolve` closure attaches the timestamp/session/stats context to
123/// the local plan; it is path-specific only in the concrete plan type it
124/// operates on.
125pub(crate) fn optimize_oneshot<O, LocalPlan, ResolvedPlan, GlobalPlan>(
126 optimizer: &mut O,
127 raw_expr: HirRelationExpr,
128 resolve: impl FnOnce(LocalPlan) -> ResolvedPlan,
129) -> Result<GlobalPlan, OptimizerError>
130where
131 O: Optimize<HirRelationExpr, To = LocalPlan> + Optimize<ResolvedPlan, To = GlobalPlan>,
132{
133 // HIR ⇒ MIR lowering and MIR optimization (local).
134 let local_mir_plan = optimizer.catch_unwind_optimize(raw_expr)?;
135 // Attach resolved context required to continue the pipeline.
136 let resolved_mir_plan = resolve(local_mir_plan);
137 // MIR optimization (global), MIR ⇒ LIR lowering, and LIR optimization (global).
138 optimizer.catch_unwind_optimize(resolved_mir_plan)
139}
140
141// Optimizer configuration
142// -----------------------
143
144/// Feature flags for the optimizer.
145///
146/// To add a new feature flag, do the following steps:
147///
148/// 1. To make the flag available to all stages in our [`Optimize`] pipelines
149/// and allow engineers to set a system-wide override:
150/// 1. Add the flag to the `optimizer_feature_flags!(...)` macro call.
151/// 2. Add the flag to the `feature_flags!(...)` macro call and extend the
152/// `From<&SystemVars>` implementation for [`OptimizerFeatures`].
153///
154/// 2. To enable `EXPLAIN ... WITH(...)` overrides which will allow engineers to
155/// inspect plan differences before deploying the optimizer changes:
156/// 1. Add the flag to the `ExplainPlanOptionName` definition.
157/// 2. Add the flag to the `generate_extracted_config!(ExplainPlanOption,
158/// ...)` macro call.
159/// 3. Extend the `TryFrom<ExplainPlanOptionExtracted>` implementation for
160/// [`mz_repr::explain::ExplainConfig`].
161///
162/// 3. To enable `CLUSTER ... FEATURES(...)` overrides which will allow
163/// engineers to experiment with runtime differences before deploying the
164/// optimizer changes:
165/// 1. Add the flag to the `ClusterFeatureName` definition.
166/// 2. Add the flag to the `generate_extracted_config!(ClusterFeature, ...)`
167/// macro call.
168/// 3. Extend the `let optimizer_feature_overrides = ...` call in
169/// `plan_create_cluster`.
170#[derive(Clone, Debug)]
171pub struct OptimizerConfig {
172 /// The mode in which the optimizer runs.
173 pub mode: OptimizeMode,
174 /// If the [`GlobalId`] is set the optimizer works in "replan" mode.
175 ///
176 /// This means that it will not consider catalog items (more specifically
177 /// indexes) with [`GlobalId`] greater or equal than the one provided here.
178 pub replan: Option<GlobalId>,
179 /// Show the slow path plan even if a fast path plan was created. Useful for debugging.
180 /// Enforced if `timing` is set.
181 pub no_fast_path: bool,
182 // If set, allow some additional queries down the Persist fast path when we believe
183 // the orderings are compatible.
184 persist_fast_path_order: bool,
185 // Enable calculating with_snapshot metadata for subscribes.
186 subscribe_snapshot_optimization: bool,
187 /// Optimizer feature flags.
188 pub features: OptimizerFeatures,
189}
190
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub enum OptimizeMode {
193 /// A mode where the optimized statement is executed.
194 Execute,
195 /// A mode where the optimized statement is explained.
196 Explain,
197}
198
199impl From<&SystemVars> for OptimizerConfig {
200 fn from(vars: &SystemVars) -> Self {
201 Self {
202 mode: OptimizeMode::Execute,
203 replan: None,
204 no_fast_path: false,
205 persist_fast_path_order: PERSIST_FAST_PATH_ORDER.get(vars.dyncfgs()),
206 subscribe_snapshot_optimization: SUBSCRIBE_SNAPSHOT_OPTIMIZATION.get(vars.dyncfgs()),
207 features: OptimizerFeatures::from(vars),
208 }
209 }
210}
211
212/// Override [`OptimizerConfig::features`] from [`OptimizerFeatureOverrides`].
213impl OverrideFrom<OptimizerFeatureOverrides> for OptimizerConfig {
214 fn override_from(mut self, overrides: &OptimizerFeatureOverrides) -> Self {
215 self.features = self.features.override_from(overrides);
216 self
217 }
218}
219
220/// [`OptimizerConfig`] overrides coming from an [`ExplainContext`].
221impl OverrideFrom<ExplainContext> for OptimizerConfig {
222 fn override_from(mut self, ctx: &ExplainContext) -> Self {
223 let ExplainContext::Plan(ctx) = ctx else {
224 return self; // Return immediately for all other contexts.
225 };
226
227 // Override general parameters.
228 self.mode = OptimizeMode::Explain;
229 self.replan = ctx.replan;
230 self.no_fast_path = ctx.config.no_fast_path;
231
232 // Override feature flags that can be enabled in the EXPLAIN config.
233 self.features = self.features.override_from(&ctx.config.features);
234
235 // Return the final result.
236 self
237 }
238}
239
240impl From<&OptimizerConfig> for mz_sql::plan::HirToMirConfig {
241 fn from(config: &OptimizerConfig) -> Self {
242 Self {
243 enable_new_outer_join_lowering: config.features.enable_new_outer_join_lowering,
244 enable_variadic_left_join_lowering: config.features.enable_variadic_left_join_lowering,
245 enable_cast_elimination: config.features.enable_cast_elimination,
246 enable_simplify_quantified_comparisons: config
247 .features
248 .enable_simplify_quantified_comparisons,
249 enable_fixed_correlated_cte_lowering: config
250 .features
251 .enable_fixed_correlated_cte_lowering,
252 enable_simplify_from_less_existence: config
253 .features
254 .enable_simplify_from_less_existence,
255 }
256 }
257}
258
259// OptimizerCatalog
260// ===============
261
262pub trait OptimizerCatalog: Debug + Send + Sync {
263 fn get_entry(&self, id: &GlobalId) -> CatalogCollectionEntry;
264 fn get_entry_by_item_id(&self, id: &CatalogItemId) -> &CatalogEntry;
265 fn resolve_full_name(
266 &self,
267 name: &QualifiedItemName,
268 conn_id: Option<&ConnectionId>,
269 ) -> FullItemName;
270
271 /// Returns all indexes on the given object and cluster known in the
272 /// catalog.
273 fn get_indexes_on(
274 &self,
275 id: GlobalId,
276 cluster: ClusterId,
277 ) -> Box<dyn Iterator<Item = (GlobalId, &Index)> + '_>;
278}
279
280// OptimizerError
281// ===============
282
283/// Error types that can be generated during optimization.
284#[derive(Debug, thiserror::Error)]
285pub enum OptimizerError {
286 #[error("{0}")]
287 PlanError(#[from] PlanError),
288 #[error("{0}")]
289 RecursionLimitError(#[from] RecursionLimitError),
290 #[error("{0}")]
291 TransformError(#[from] TransformError),
292 #[error("{0}")]
293 EvalError(#[from] EvalError),
294 #[error("cannot materialize call to {0}")]
295 UnmaterializableFunction(UnmaterializableFunc),
296 #[error("cannot call {func} in {context} ")]
297 UncallableFunction {
298 func: UnmaterializableFunc,
299 context: &'static str,
300 },
301 #[error("access to function {0} is restricted")]
302 RestrictedFunction(UnmaterializableFunc),
303 #[error("{0}")]
304 UnsupportedTemporalExpression(String),
305 /// This is a specific kind of internal error. It's distinct from `Internal`, because we want to
306 /// catch it and swallow it in some cases.
307 #[error("internal optimizer error: MfpPlan couldn't be converted into SafeMfpPlan")]
308 InternalUnsafeMfpPlan(String),
309 #[error("internal optimizer error: {0}")]
310 Internal(String),
311}
312
313impl From<String> for OptimizerError {
314 fn from(msg: String) -> Self {
315 Self::Internal(msg)
316 }
317}
318
319impl OptimizerError {
320 pub fn detail(&self) -> Option<String> {
321 match self {
322 Self::UnmaterializableFunction(UnmaterializableFunc::CurrentTimestamp) => {
323 Some("See: https://materialize.com/docs/sql/functions/now_and_mz_now/".into())
324 }
325 Self::RestrictedFunction(_) => Some(
326 "Access to system catalog objects is restricted for this role. \
327 Contact your administrator if you need access."
328 .into(),
329 ),
330 _ => None,
331 }
332 }
333
334 pub fn hint(&self) -> Option<String> {
335 match self {
336 Self::UnmaterializableFunction(UnmaterializableFunc::CurrentTimestamp) => {
337 Some("In temporal filters `mz_now()` may work instead.".into())
338 }
339 _ => None,
340 }
341 }
342}
343
344impl From<TimestampError> for OptimizerError {
345 fn from(value: TimestampError) -> Self {
346 OptimizerError::EvalError(EvalError::from(value))
347 }
348}
349
350impl From<anyhow::Error> for OptimizerError {
351 fn from(value: anyhow::Error) -> Self {
352 OptimizerError::Internal(value.to_string())
353 }
354}
355
356impl MaybeShouldPanic for OptimizerError {
357 fn should_panic(&self) -> Option<String> {
358 match self {
359 OptimizerError::TransformError(TransformError::CallerShouldPanic(msg)) => {
360 Some(msg.to_string())
361 }
362 _ => None,
363 }
364 }
365}
366
367// Tracing helpers
368// ---------------
369
370#[mz_ore::instrument(target = "optimizer", level = "debug", name = "local")]
371fn optimize_mir_local(
372 expr: MirRelationExpr,
373 ctx: &mut TransformCtx,
374) -> Result<OptimizedMirRelationExpr, OptimizerError> {
375 fail::fail_point!("optimize_mir_local");
376
377 #[allow(deprecated)]
378 let optimizer = mz_transform::Optimizer::logical_optimizer(ctx);
379 let expr = optimizer.optimize(expr, ctx)?;
380
381 // Trace the result of this phase.
382 mz_repr::explain::trace_plan(expr.as_inner());
383
384 Ok::<_, OptimizerError>(expr)
385}
386
387/// This is just a wrapper around [mz_transform::Optimizer::constant_optimizer],
388/// running it, and tracing the result plan.
389#[mz_ore::instrument(target = "optimizer", level = "debug", name = "constant")]
390fn optimize_mir_constant(
391 expr: MirRelationExpr,
392 ctx: &mut TransformCtx,
393 limit: bool,
394) -> Result<MirRelationExpr, OptimizerError> {
395 let optimizer = mz_transform::Optimizer::constant_optimizer(ctx, limit);
396 let expr = optimizer.optimize(expr, ctx)?;
397
398 // Trace the result of this phase.
399 mz_repr::explain::trace_plan(expr.as_inner());
400
401 Ok::<_, OptimizerError>(expr.0)
402}
403
404macro_rules! trace_plan {
405 (at: $span:literal, $plan:expr) => {
406 tracing::debug_span!(target: "optimizer", $span).in_scope(|| {
407 mz_repr::explain::trace_plan($plan);
408 });
409 }
410}
411
412use trace_plan;
413
414use crate::coord::ExplainContext;