Skip to main content

mz_expr/
explain.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//! `EXPLAIN` support for structures defined in this crate.
11
12use std::borrow::Cow;
13use std::collections::BTreeMap;
14use std::fmt::Formatter;
15use std::time::Duration;
16
17use mz_ore::str::{Indent, IndentLike, separated};
18use mz_repr::GlobalId;
19use mz_repr::explain::ExplainError::LinearChainsPlusRecursive;
20use mz_repr::explain::text::DisplayText;
21use mz_repr::explain::{
22    AnnotatedPlan, Explain, ExplainConfig, ExplainError, ExprHumanizer, ScalarOps,
23    UnsupportedFormat, UsedIndexes,
24};
25use mz_repr::optimize::OptimizerFeatures;
26
27use crate::interpret::{Interpretable, Interpreter, MfpEval, Trace};
28use crate::scalar::optimizable::OptimizableExpr;
29use crate::visit::Visit;
30use crate::{
31    AccessStrategy, BinaryFunc, Id, LocalId, MapFilterProject, MfpPlan, MirRelationExpr,
32    MirScalarExpr, RowSetFinishing, UnmaterializableFunc, func,
33};
34
35pub use crate::explain::text::{
36    HumanizeDisplay, HumanizedExplain, HumanizedExpr, HumanizedNotice, HumanizerMode,
37    fmt_text_constant_rows,
38};
39
40mod json;
41mod text;
42
43/// Explain context shared by all [`mz_repr::explain::Explain`]
44/// implementations in this crate.
45#[derive(Debug)]
46pub struct ExplainContext<'a> {
47    pub config: &'a ExplainConfig,
48    pub features: &'a OptimizerFeatures,
49    pub humanizer: &'a dyn ExprHumanizer,
50    pub cardinality_stats: BTreeMap<GlobalId, usize>,
51    pub used_indexes: UsedIndexes,
52    pub finishing: Option<RowSetFinishing>,
53    pub duration: Duration,
54    // Cluster against which the explained plan is optimized.
55    pub target_cluster: Option<&'a str>,
56    // This is a String so that we don't have to move `OptimizerNotice` to `mz-expr`. We can revisit
57    // this decision if we want to every make this print in the json output in a machine readable
58    // way.
59    pub optimizer_notices: Vec<String>,
60}
61
62/// A structure produced by the `explain_$format` methods in
63/// [`mz_repr::explain::Explain`] implementations for points
64/// in the optimization pipeline identified with a single plan of
65/// type `T`.
66#[allow(missing_debug_implementations)]
67pub struct ExplainSinglePlan<'a, T> {
68    /// Configuration for the explain
69    pub context: &'a ExplainContext<'a>,
70    /// The plan to explain
71    pub plan: AnnotatedPlan<'a, T>,
72}
73
74/// Carries metadata about the possibility of MFP pushdown for a source.
75/// (Likely to change, and only emitted when a context flag is enabled.)
76/// The operators of a source import that persist filter pushdown can apply,
77/// as `EXPLAIN ... WITH (filter pushdown)` reports them.
78#[allow(missing_debug_implementations)]
79pub struct PushdownInfo<E = MirScalarExpr> {
80    /// Pushdown-able filters in the source, by index.
81    pub pushdown: Vec<E>,
82    /// Pushdown-able lower temporal bounds, each a `bound <= mz_now()`.
83    pub lower_bounds: Vec<E>,
84    /// Pushdown-able upper temporal bounds, each a `mz_now() < bound`.
85    pub upper_bounds: Vec<E>,
86}
87
88impl<'a, C, M, E> DisplayText<C> for HumanizedExpr<'a, PushdownInfo<E>, M>
89where
90    C: AsMut<Indent>,
91    M: HumanizerMode,
92    E: HumanizeDisplay,
93{
94    fn fmt_text(&self, f: &mut Formatter<'_>, ctx: &mut C) -> std::fmt::Result {
95        let PushdownInfo {
96            pushdown,
97            lower_bounds,
98            upper_bounds,
99        } = self.expr;
100
101        if !pushdown.is_empty() {
102            let pushdown = pushdown.iter().map(|e| self.mode.expr(e, self.cols));
103            let pushdown = separated(" AND ", pushdown);
104            writeln!(f, "{}pushdown=({})", ctx.as_mut(), pushdown)?;
105        }
106        if !lower_bounds.is_empty() || !upper_bounds.is_empty() {
107            write!(f, "{}pushdown ", ctx.as_mut())?;
108            fmt_temporal_filter(lower_bounds, upper_bounds, self, f)?;
109        }
110
111        Ok(())
112    }
113}
114
115/// Renders `temporal filter=<lowers> <= mz_now() < <uppers>`, omitting the
116/// `<lowers> <=` or `< <uppers>` part when the corresponding list is empty,
117/// and ends the line.
118pub fn fmt_temporal_filter<'a, T, M, E>(
119    lower_bounds: &[E],
120    upper_bounds: &[E],
121    humanizer: &HumanizedExpr<'a, T, M>,
122    f: &mut Formatter<'_>,
123) -> std::fmt::Result
124where
125    M: HumanizerMode,
126    E: HumanizeDisplay,
127{
128    write!(f, "temporal filter=")?;
129    if !lower_bounds.is_empty() {
130        let lowers = lower_bounds.iter().map(|b| humanizer.child(b));
131        write!(f, "{} <= ", separated(", ", lowers))?;
132    }
133    write!(f, "mz_now()")?;
134    if !upper_bounds.is_empty() {
135        let uppers = upper_bounds.iter().map(|b| humanizer.child(b));
136        write!(f, " < {}", separated(", ", uppers))?;
137    }
138    writeln!(f)
139}
140
141/// The operators pushed down onto a source import, in either form a
142/// `DataflowDescription` carries them: the `MapFilterProject` the MIR
143/// optimizer rewrites, or the `MfpPlan` lowering leaves.
144pub trait SourceOperators: Clone + serde::Serialize {
145    /// The scalar expression type of the operators.
146    type Expr: HumanizeDisplay + serde::Serialize;
147
148    /// Whether the operators pass rows through unchanged.
149    fn is_identity(&self) -> bool;
150
151    /// How many columns the map stage appends to the source's rows.
152    fn appended_columns(&self) -> usize;
153
154    /// Which of the operators persist filter pushdown can apply.
155    fn pushdown_info(&self) -> PushdownInfo<Self::Expr>;
156
157    /// Renders the operators, one `key=(...)` line each.
158    fn fmt_text<M: HumanizerMode, C: AsMut<Indent>>(
159        humanized: &HumanizedExpr<'_, Self, M>,
160        f: &mut Formatter<'_>,
161        ctx: &mut C,
162    ) -> std::fmt::Result;
163}
164
165impl<E> SourceOperators for MapFilterProject<E>
166where
167    E: OptimizableExpr + HumanizeDisplay + Interpretable + serde::Serialize,
168{
169    type Expr = E;
170
171    fn is_identity(&self) -> bool {
172        MapFilterProject::is_identity(self)
173    }
174
175    fn appended_columns(&self) -> usize {
176        self.expressions.len()
177    }
178
179    fn pushdown_info(&self) -> PushdownInfo<E> {
180        let mfp_mapped = MfpEval::new(&Trace, self.input_arity, &self.expressions);
181        let pushdown = self
182            .predicates
183            .iter()
184            .filter(|(_, e)| mfp_mapped.expr(e).pushdownable())
185            .map(|(_, e)| e.clone())
186            .collect();
187        PushdownInfo {
188            pushdown,
189            lower_bounds: Vec::new(),
190            upper_bounds: Vec::new(),
191        }
192    }
193
194    fn fmt_text<M: HumanizerMode, C: AsMut<Indent>>(
195        humanized: &HumanizedExpr<'_, Self, M>,
196        f: &mut Formatter<'_>,
197        ctx: &mut C,
198    ) -> std::fmt::Result {
199        DisplayText::fmt_text(humanized, f, ctx)
200    }
201}
202
203impl<E> SourceOperators for MfpPlan<E>
204where
205    E: OptimizableExpr + HumanizeDisplay + Interpretable + serde::Serialize,
206{
207    type Expr = E;
208
209    fn is_identity(&self) -> bool {
210        MfpPlan::is_identity(self)
211    }
212
213    fn appended_columns(&self) -> usize {
214        self.safe_mfp().expressions.len()
215    }
216
217    fn pushdown_info(&self) -> PushdownInfo<E> {
218        let (safe, lower_bounds, upper_bounds) = self.as_parts();
219        let mfp_mapped = MfpEval::new(&Trace, safe.input_arity, &safe.expressions);
220        let pushdown = safe
221            .predicates
222            .iter()
223            .filter(|(_, e)| mfp_mapped.expr(e).pushdownable())
224            .map(|(_, e)| e.clone())
225            .collect();
226        // A bound is pushed down as the comparison `mfp_plan_filter`
227        // evaluates it against the part's `mz_now()` range.
228        let mz_now = mfp_mapped.unmaterializable(&UnmaterializableFunc::MzNow);
229        let pushdownable = |bound: &E, func: BinaryFunc| {
230            mfp_mapped
231                .binary(&func, mfp_mapped.expr(bound), mz_now.clone())
232                .pushdownable()
233        };
234        let lower_bounds = lower_bounds
235            .iter()
236            .filter(|b| pushdownable(b, BinaryFunc::Lte(func::Lte)))
237            .cloned()
238            .collect();
239        let upper_bounds = upper_bounds
240            .iter()
241            .filter(|b| pushdownable(b, BinaryFunc::Gte(func::Gte)))
242            .cloned()
243            .collect();
244        PushdownInfo {
245            pushdown,
246            lower_bounds,
247            upper_bounds,
248        }
249    }
250
251    fn fmt_text<M: HumanizerMode, C: AsMut<Indent>>(
252        humanized: &HumanizedExpr<'_, Self, M>,
253        f: &mut Formatter<'_>,
254        ctx: &mut C,
255    ) -> std::fmt::Result {
256        let (safe, lower_bounds, upper_bounds) = humanized.expr.as_parts();
257        DisplayText::fmt_text(&humanized.child(&**safe), f, ctx)?;
258        if !lower_bounds.is_empty() || !upper_bounds.is_empty() {
259            write!(f, "{}", ctx.as_mut())?;
260            fmt_temporal_filter(lower_bounds, upper_bounds, humanized, f)?;
261        }
262        Ok(())
263    }
264}
265
266#[allow(missing_debug_implementations)]
267pub struct ExplainSource<'a, O: SourceOperators = MapFilterProject> {
268    pub id: GlobalId,
269    pub op: Option<Cow<'a, O>>,
270    pub pushdown_info: Option<PushdownInfo<O::Expr>>,
271}
272
273impl<'a, O: SourceOperators> ExplainSource<'a, O> {
274    pub fn new(
275        id: GlobalId,
276        op: Option<Cow<'a, O>>,
277        filter_pushdown: bool,
278    ) -> ExplainSource<'a, O> {
279        let pushdown_info = if filter_pushdown {
280            op.as_deref().map(SourceOperators::pushdown_info)
281        } else {
282            None
283        };
284
285        ExplainSource {
286            id,
287            op,
288            pushdown_info,
289        }
290    }
291
292    #[inline]
293    pub fn is_identity(&self) -> bool {
294        match &self.op {
295            Some(op) => op.is_identity(),
296            None => false,
297        }
298    }
299}
300
301impl<'a, 'h, C, M, O> DisplayText<C> for HumanizedExpr<'a, ExplainSource<'a, O>, M>
302where
303    C: AsMut<Indent> + AsRef<&'h dyn ExprHumanizer>,
304    M: HumanizerMode,
305    O: SourceOperators,
306{
307    fn fmt_text(&self, f: &mut std::fmt::Formatter<'_>, ctx: &mut C) -> std::fmt::Result {
308        let id = ctx
309            .as_ref()
310            .humanize_id(self.expr.id)
311            .unwrap_or_else(|| self.expr.id.to_string());
312        writeln!(f, "{}Source {}", ctx.as_mut(), id)?;
313        ctx.indented(|ctx| {
314            if let Some(op) = self.expr.op.as_deref() {
315                O::fmt_text(&self.child(op), f, ctx)?;
316            }
317            if let Some(pushdown_info) = &self.expr.pushdown_info {
318                self.child(pushdown_info).fmt_text(f, ctx)?;
319            }
320            Ok(())
321        })
322    }
323}
324
325/// A structure produced by the `explain_$format` methods in
326/// [`mz_repr::explain::Explain`] implementations at points
327/// in the optimization pipeline identified with a
328/// `DataflowDescription` instance with plans of type `T`.
329#[allow(missing_debug_implementations)]
330pub struct ExplainMultiPlan<'a, T, O: SourceOperators = MapFilterProject> {
331    pub context: &'a ExplainContext<'a>,
332    // Maps the names of the sources to the linear operators that will be
333    // on them.
334    pub sources: Vec<ExplainSource<'a, O>>,
335    // elements of the vector are in topological order
336    pub plans: Vec<(String, AnnotatedPlan<'a, T>)>,
337}
338
339impl<'a> Explain<'a> for MirRelationExpr {
340    type Context = ExplainContext<'a>;
341
342    type Text = ExplainSinglePlan<'a, MirRelationExpr>;
343
344    type Json = ExplainSinglePlan<'a, MirRelationExpr>;
345
346    type Dot = UnsupportedFormat;
347
348    fn explain_text(&'a mut self, context: &'a Self::Context) -> Result<Self::Text, ExplainError> {
349        self.as_explain_single_plan(context)
350    }
351
352    fn explain_json(&'a mut self, context: &'a Self::Context) -> Result<Self::Json, ExplainError> {
353        self.as_explain_single_plan(context)
354    }
355}
356
357impl<'a> MirRelationExpr {
358    fn as_explain_single_plan(
359        &'a mut self,
360        context: &'a ExplainContext<'a>,
361    ) -> Result<ExplainSinglePlan<'a, MirRelationExpr>, ExplainError> {
362        // normalize the representation as linear chains
363        // (this implies !context.config.raw_plans by construction)
364        if context.config.linear_chains {
365            enforce_linear_chains(self)?;
366        };
367
368        let plan = AnnotatedPlan {
369            plan: self,
370            annotations: BTreeMap::default(),
371        };
372
373        Ok(ExplainSinglePlan { context, plan })
374    }
375}
376
377/// Normalize the way inputs of multi-input variants are rendered.
378///
379/// After the transform is applied, non-trival inputs `$input` of variants with
380/// more than one input are wrapped in a `let $x = $input in $x` blocks.
381///
382/// If these blocks are subsequently pulled up by `NormalizeLets`,
383/// the rendered version of the resulting tree will only have linear chains.
384pub fn enforce_linear_chains(expr: &mut MirRelationExpr) -> Result<(), ExplainError> {
385    use MirRelationExpr::{Constant, Get, Join, Union};
386
387    if expr.is_recursive() {
388        // `linear_chains` is not implemented for WMR, see
389        // https://github.com/MaterializeInc/database-issues/issues/5631
390        return Err(LinearChainsPlusRecursive);
391    }
392
393    // helper struct: a generator of fresh local ids
394    let mut id_gen = id_gen(expr).peekable();
395
396    let mut wrap_in_let = |input: &mut MirRelationExpr| {
397        match input {
398            Constant { .. } | Get { .. } => (),
399            input => {
400                // generate fresh local id
401                // let id = id_cnt
402                //     .next()
403                //     .map(|id| LocalId::new(1000_u64 + u64::cast_from(id_map.len()) + id))
404                //     .unwrap();
405                let id = id_gen.next().unwrap();
406                let value = input.take_safely(None);
407                // generate a `let $fresh_id = $body in $fresh_id` to replace this input
408                let mut binding = MirRelationExpr::Let {
409                    id,
410                    value: Box::new(value),
411                    body: Box::new(Get {
412                        id: Id::Local(id.clone()),
413                        typ: input.typ(),
414                        access_strategy: AccessStrategy::UnknownOrLocal,
415                    }),
416                };
417                // swap the current body with the replacement
418                std::mem::swap(input, &mut binding);
419            }
420        }
421    };
422
423    expr.try_visit_mut_post(&mut |expr: &mut MirRelationExpr| {
424        match expr {
425            Join { inputs, .. } => {
426                for input in inputs {
427                    wrap_in_let(input);
428                }
429            }
430            Union { base, inputs } => {
431                wrap_in_let(base);
432                for input in inputs {
433                    wrap_in_let(input);
434                }
435            }
436            _ => (),
437        }
438        Ok(())
439    })
440}
441
442// Create an [`Iterator`] for [`LocalId`] values that are guaranteed to be
443// fresh within the scope of the given [`MirRelationExpr`].
444fn id_gen(expr: &MirRelationExpr) -> impl Iterator<Item = LocalId> + use<> {
445    let mut max_id = 0_u64;
446
447    expr.visit_pre(|expr| {
448        match expr {
449            MirRelationExpr::Let { id, .. } => max_id = std::cmp::max(max_id, id.into()),
450            _ => (),
451        };
452    });
453
454    (max_id + 1..).map(LocalId::new)
455}
456
457impl ScalarOps for MirScalarExpr {
458    fn match_col_ref(&self) -> Option<usize> {
459        match self {
460            MirScalarExpr::Column(c, _name) => Some(*c),
461            _ => None,
462        }
463    }
464
465    fn references(&self, column: usize) -> bool {
466        match self {
467            MirScalarExpr::Column(c, _name) => *c == column,
468            _ => false,
469        }
470    }
471}