Skip to main content

mz_adapter/optimize/
subscribe.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 implementation for `SUBSCRIBE` statements.
11
12use std::marker::PhantomData;
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16use differential_dataflow::lattice::Lattice;
17use mz_compute_types::ComputeInstanceId;
18use mz_compute_types::plan::LirRelationExpr;
19use mz_compute_types::sinks::{ComputeSinkConnection, ComputeSinkDesc, SubscribeSinkConnection};
20use mz_expr::{ColumnOrder, MirRelationExpr};
21use mz_ore::soft_assert_or_log;
22use mz_repr::{GlobalId, RelationDesc, Timestamp};
23use mz_sql::optimizer_metrics::OptimizerMetrics;
24use mz_sql::plan::{HirToMirConfig, SubscribeFrom, SubscribePlan};
25use mz_transform::TransformCtx;
26use mz_transform::dataflow::{DataflowMetainfo, optimize_dataflow_snapshot};
27use mz_transform::normalize_lets::normalize_lets;
28use mz_transform::typecheck::{SharedTypecheckingContext, empty_typechecking_context};
29use timely::progress::Antichain;
30
31use crate::CollectionIdBundle;
32use crate::optimize::dataflows::{
33    ComputeInstanceSnapshot, DataflowBuilder, ExprPrep, ExprPrepMaintained,
34    dataflow_import_id_bundle,
35};
36use crate::optimize::{
37    LirDataflowDescription, MirDataflowDescription, Optimize, OptimizeMode, OptimizerCatalog,
38    OptimizerConfig, OptimizerError, optimize_mir_local, trace_plan,
39};
40
41pub struct Optimizer {
42    /// A representation typechecking context to use throughout the optimizer pipeline.
43    typecheck_ctx: SharedTypecheckingContext,
44    /// A snapshot of the catalog state.
45    catalog: Arc<dyn OptimizerCatalog>,
46    /// A snapshot of the cluster that will run the dataflows.
47    compute_instance: ComputeInstanceSnapshot,
48    /// A transient GlobalId to be used for the exported sink.
49    sink_id: GlobalId,
50    /// A transient GlobalId to be used when constructing a dataflow for
51    /// `SUBSCRIBE FROM <SELECT>` variants.
52    view_id: GlobalId,
53    /// Should the plan produce an initial snapshot?
54    with_snapshot: bool,
55    /// Sink timestamp.
56    up_to: Option<Timestamp>,
57    /// A human-readable name exposed internally (useful for debugging).
58    debug_name: String,
59    /// Optimizer config.
60    config: OptimizerConfig,
61    /// Optimizer metrics.
62    metrics: OptimizerMetrics,
63    /// The time spent performing optimization so far.
64    duration: Duration,
65}
66
67// A bogey `Debug` implementation that hides fields, so that debug-formatting an
68// optimizer (e.g. in a tracing event) does not emit a lot of data.
69//
70// For now, we skip almost all fields, but we might revisit that bit if it turns
71// out that we really need those for debugging purposes.
72impl std::fmt::Debug for Optimizer {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("Optimizer")
75            .field("config", &self.config)
76            .finish_non_exhaustive()
77    }
78}
79
80impl Optimizer {
81    pub fn new(
82        catalog: Arc<dyn OptimizerCatalog>,
83        compute_instance: ComputeInstanceSnapshot,
84        view_id: GlobalId,
85        sink_id: GlobalId,
86        with_snapshot: bool,
87        up_to: Option<Timestamp>,
88        debug_name: String,
89        config: OptimizerConfig,
90        metrics: OptimizerMetrics,
91    ) -> Self {
92        Self {
93            typecheck_ctx: empty_typechecking_context(),
94            catalog,
95            compute_instance,
96            view_id,
97            sink_id,
98            with_snapshot,
99            up_to,
100            debug_name,
101            config,
102            metrics,
103            duration: Default::default(),
104        }
105    }
106
107    pub fn cluster_id(&self) -> ComputeInstanceId {
108        self.compute_instance.instance_id()
109    }
110
111    pub fn up_to(&self) -> Option<Timestamp> {
112        self.up_to.clone()
113    }
114
115    pub fn sink_id(&self) -> GlobalId {
116        self.sink_id
117    }
118
119    pub fn config(&self) -> &OptimizerConfig {
120        &self.config
121    }
122
123    /// Optimizes a subscribe over an already-lowered MIR expression, for
124    /// callers that build their own MIR (such as the frontend read-then-write
125    /// path, which applies the mutation in MIR).
126    ///
127    /// `output` is the sink's row ordering, as produced by
128    /// [`mz_sql::plan::SubscribeOutput::row_order`]. Empty means the sink emits
129    /// raw diffs.
130    pub fn optimize_query(
131        &mut self,
132        expr: MirRelationExpr,
133        from_desc: RelationDesc,
134        output: Vec<ColumnOrder>,
135    ) -> Result<GlobalMirPlan<Unresolved>, OptimizerError> {
136        self.optimize_inner(SubscribeSource::Query { expr, from_desc }, output)
137    }
138
139    /// The single subscribe optimization pipeline. Every subscribe, whatever it
140    /// reads from, goes through here, so a prep or metainfo step added here
141    /// applies to all of them.
142    fn optimize_inner(
143        &mut self,
144        source: SubscribeSource,
145        output: Vec<ColumnOrder>,
146    ) -> Result<GlobalMirPlan<Unresolved>, OptimizerError> {
147        let time = Instant::now();
148
149        let mut df_builder = {
150            let compute = self.compute_instance.clone();
151            DataflowBuilder::new(&*self.catalog, compute).with_config(&self.config)
152        };
153        let mut df_desc = MirDataflowDescription::new(self.debug_name.clone());
154        let mut df_meta = DataflowMetainfo::default();
155
156        let (from, from_desc) = match source {
157            SubscribeSource::Id(from_id) => {
158                let from_desc = self
159                    .catalog
160                    .get_entry(&from_id)
161                    .relation_desc()
162                    .expect("subscribes can only be run on items with descs")
163                    .into_owned();
164
165                df_builder.import_into_dataflow(&from_id, &mut df_desc, &self.config.features)?;
166
167                (from_id, from_desc)
168            }
169            SubscribeSource::Query { expr, from_desc } => {
170                // MIR ⇒ MIR optimization (local)
171                let mut transform_ctx = TransformCtx::local(
172                    &self.config.features,
173                    &self.typecheck_ctx,
174                    &mut df_meta,
175                    Some(&mut self.metrics),
176                    Some(self.view_id),
177                );
178                let expr = optimize_mir_local(expr, &mut transform_ctx)?;
179
180                df_builder.import_view_into_dataflow(
181                    &self.view_id,
182                    &expr,
183                    &mut df_desc,
184                    &self.config.features,
185                )?;
186
187                (self.view_id, from_desc)
188            }
189        };
190        df_builder.maybe_reoptimize_imported_views(&mut df_desc, &self.config)?;
191
192        // Make SinkDesc
193        let sink_description = ComputeSinkDesc {
194            from,
195            from_desc,
196            connection: ComputeSinkConnection::Subscribe(SubscribeSinkConnection { output }),
197            with_snapshot: self.with_snapshot,
198            up_to: self.up_to.map(Antichain::from_elem).unwrap_or_default(),
199            // No `FORCE NOT NULL` for subscribes
200            non_null_assertions: vec![],
201            // No `REFRESH` for subscribes
202            refresh_schedule: None,
203            // Populated during LIR lowering.
204            from_arrangement: None,
205        };
206        df_desc.export_sink(self.sink_id, sink_description);
207
208        // Prepare expressions in the assembled dataflow.
209        let style = ExprPrepMaintained;
210        df_desc.visit_children(
211            |r| style.prep_relation_expr(r),
212            |s| style.prep_scalar_expr(s),
213        )?;
214
215        // Construct TransformCtx for global optimization.
216        let mut transform_ctx = TransformCtx::global(
217            &df_builder,
218            &mz_transform::EmptyStatisticsOracle, // TODO: wire proper stats
219            &self.config.features,
220            &self.typecheck_ctx,
221            &mut df_meta,
222            Some(&mut self.metrics),
223        );
224        // Run global optimization.
225        mz_transform::optimize_dataflow(&mut df_desc, &mut transform_ctx, false)?;
226
227        if self.config.mode == OptimizeMode::Explain {
228            // Collect the list of indexes used by the dataflow at this point.
229            trace_plan!(at: "global", &df_meta.used_indexes(&df_desc));
230        }
231
232        self.duration += time.elapsed();
233
234        // Return the (sealed) plan at the end of this optimization step.
235        Ok(GlobalMirPlan {
236            df_desc,
237            df_meta,
238            phantom: PhantomData::<Unresolved>,
239        })
240    }
241}
242
243/// What a subscribe reads from, with any HIR ⇒ MIR lowering already done.
244enum SubscribeSource {
245    /// An existing collection, imported by id.
246    Id(GlobalId),
247    /// A query, imported into the dataflow as a view under `view_id`.
248    Query {
249        expr: MirRelationExpr,
250        from_desc: RelationDesc,
251    },
252}
253
254/// The (sealed intermediate) result after:
255///
256/// 1. embedding a [`SubscribeFrom`] plan into a [`MirDataflowDescription`],
257/// 2. transitively inlining referenced views, and
258/// 3. jointly optimizing the `MIR` plans in the [`MirDataflowDescription`].
259#[derive(Clone, Debug)]
260pub struct GlobalMirPlan<T: Clone> {
261    df_desc: MirDataflowDescription,
262    df_meta: DataflowMetainfo,
263    phantom: PhantomData<T>,
264}
265
266impl<T: Clone> GlobalMirPlan<T> {
267    /// Computes the [`CollectionIdBundle`] of the wrapped dataflow.
268    pub fn id_bundle(&self, compute_instance_id: ComputeInstanceId) -> CollectionIdBundle {
269        dataflow_import_id_bundle(&self.df_desc, compute_instance_id)
270    }
271}
272
273/// The (final) result after MIR ⇒ LIR lowering and optimizing the resulting
274/// `DataflowDescription` with `LIR` plans.
275#[derive(Clone, Debug)]
276pub struct GlobalLirPlan {
277    df_desc: LirDataflowDescription,
278    df_meta: DataflowMetainfo,
279}
280
281impl GlobalLirPlan {
282    /// Returns the id of the dataflow's sink export.
283    ///
284    /// # Panics
285    ///
286    /// Panics if the dataflow has no sink exports or has more than one.
287    pub fn sink_id(&self) -> GlobalId {
288        self.df_desc.sink_id()
289    }
290}
291
292/// Marker type for [`GlobalMirPlan`] structs representing an optimization
293/// result without a resolved timestamp.
294#[derive(Clone, Debug)]
295pub struct Unresolved;
296
297/// Marker type for [`GlobalMirPlan`] structs representing an optimization
298/// result with a resolved timestamp.
299///
300/// The actual timestamp value is set in the [`MirDataflowDescription`] of the
301/// surrounding [`GlobalMirPlan`] when we call `resolve()`.
302#[derive(Clone, Debug)]
303pub struct Resolved;
304
305impl Optimize<SubscribePlan> for Optimizer {
306    type To = GlobalMirPlan<Unresolved>;
307
308    fn optimize(&mut self, plan: SubscribePlan) -> Result<Self::To, OptimizerError> {
309        let output = plan.output.row_order().to_vec();
310
311        match plan.from {
312            SubscribeFrom::Id(from_id) => self.optimize_inner(SubscribeSource::Id(from_id), output),
313            SubscribeFrom::Query { expr, desc } => {
314                // TODO: Change the `expr` type to be `HirRelationExpr` and run
315                // HIR ⇒ MIR lowering and decorrelation here. This would allow
316                // us implement something like `EXPLAIN RAW PLAN FOR SUBSCRIBE.`
317                //
318                // let typ = expr.top_level_typ();
319
320                // Lowering counts towards the reported optimization time, so we
321                // time it here and `optimize_inner` times the rest.
322                let time = Instant::now();
323                let expr = expr.lower(HirToMirConfig::from(&self.config), None)?;
324                self.duration += time.elapsed();
325
326                self.optimize_inner(
327                    SubscribeSource::Query {
328                        expr,
329                        from_desc: desc,
330                    },
331                    output,
332                )
333            }
334        }
335    }
336}
337
338impl GlobalMirPlan<Unresolved> {
339    /// Produces the [`GlobalMirPlan`] with [`Resolved`] timestamp.
340    ///
341    /// We need to resolve timestamps before the `GlobalMirPlan ⇒ GlobalLirPlan`
342    /// optimization stage in order to profit from possible single-time
343    /// optimizations in the `Plan::finalize_dataflow` call.
344    pub fn resolve(mut self, as_of: Antichain<Timestamp>) -> GlobalMirPlan<Resolved> {
345        // A dataflow description for a `SUBSCRIBE` statement should not have
346        // index exports.
347        soft_assert_or_log!(
348            self.df_desc.index_exports.is_empty(),
349            "unexpectedly setting until for a DataflowDescription with an index",
350        );
351
352        // Set the `as_of` timestamp for the dataflow.
353        self.df_desc.set_as_of(as_of);
354
355        // The only outputs of the dataflow are sinks, so we might be able to
356        // turn off the computation early, if they all have non-trivial
357        // `up_to`s.
358        self.df_desc.until = Antichain::from_elem(Timestamp::MIN);
359        for (_, sink) in &self.df_desc.sink_exports {
360            self.df_desc.until.join_assign(&sink.up_to);
361        }
362
363        GlobalMirPlan {
364            df_desc: self.df_desc,
365            df_meta: self.df_meta,
366            phantom: PhantomData::<Resolved>,
367        }
368    }
369}
370
371impl Optimize<GlobalMirPlan<Resolved>> for Optimizer {
372    type To = GlobalLirPlan;
373
374    fn optimize(&mut self, plan: GlobalMirPlan<Resolved>) -> Result<Self::To, OptimizerError> {
375        let time = Instant::now();
376
377        let GlobalMirPlan {
378            mut df_desc,
379            df_meta,
380            phantom: _,
381        } = plan;
382
383        // Ensure all expressions are normalized before finalizing.
384        for build in df_desc.objects_to_build.iter_mut() {
385            normalize_lets(&mut build.plan.0, &self.config.features)?
386        }
387
388        if self.config.subscribe_snapshot_optimization {
389            // Determine whether we can elide any snapshots for this subscribe.
390            optimize_dataflow_snapshot(&mut df_desc)?;
391        }
392
393        // Finalize the dataflow. This includes:
394        // - MIR ⇒ LIR lowering
395        // - LIR ⇒ LIR transforms
396        let df_desc = LirRelationExpr::finalize_dataflow(
397            df_desc,
398            &self.config.features,
399            Some(self.metrics.lowering()),
400        )?;
401
402        self.duration += time.elapsed();
403        self.metrics
404            .observe_e2e_optimization_time("subscribe", self.duration);
405
406        // Return the plan at the end of this `optimize` step.
407        Ok(GlobalLirPlan { df_desc, df_meta })
408    }
409}
410
411impl GlobalLirPlan {
412    /// Unwraps the parts of the final result of the optimization pipeline.
413    pub fn unapply(self) -> (LirDataflowDescription, DataflowMetainfo) {
414        (self.df_desc, self.df_meta)
415    }
416}