mz_compute_types/plan.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//! An explicit representation of a rendering plan for provided dataflows.
11
12#![warn(missing_debug_implementations)]
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use columnar::Columnar;
17use mz_expr::{
18 CollectionPlan, EvalError, Id, LetRecLimit, LocalId, MapFilterProject, MfpPlan,
19 OptimizedMirRelationExpr, SafeMfpPlan, TableFunc,
20};
21use mz_ore::metric;
22use mz_ore::metrics::MetricsRegistry;
23use mz_ore::metrics::raw::IntCounterVec;
24use mz_ore::soft_assert_eq_no_log;
25use mz_ore::str::Indent;
26use mz_repr::explain::text::text_string_at;
27use mz_repr::explain::{DummyHumanizer, ExplainConfig, ExprHumanizer, PlanRenderingContext};
28use mz_repr::optimize::OptimizerFeatures;
29use mz_repr::{Diff, GlobalId, StableRow, Timestamp};
30use serde::{Deserialize, Serialize};
31
32use crate::dataflows::{DataflowDescription, LirDataflowDescription};
33use crate::plan::join::JoinPlan;
34use crate::plan::reduce::{KeyValPlan, ReducePlan};
35use crate::plan::scalar::LirScalarExpr;
36use crate::plan::threshold::ThresholdPlan;
37use crate::plan::top_k::TopKPlan;
38use crate::plan::transform::{Transform, TransformConfig};
39
40mod lowering;
41pub mod pinned;
42
43pub mod interpret;
44pub mod join;
45pub mod reduce;
46pub mod render_plan;
47pub mod scalar;
48pub mod threshold;
49pub mod top_k;
50pub mod transform;
51
52/// Metrics collected during MIR to LIR lowering.
53#[derive(Debug, Clone)]
54pub struct LoweringMetrics {
55 /// Counts non-`None` results of `MapFilterProject::literal_constraints` during lowering,
56 /// labeled by the call site (`"get"` or `"mfp"`).
57 literal_constraints: IntCounterVec,
58}
59
60impl LoweringMetrics {
61 /// Registers the lowering metrics into `registry`.
62 pub fn register_into(registry: &MetricsRegistry) -> Self {
63 Self {
64 literal_constraints: registry.register(metric!(
65 name: "mz_optimizer_lowering_literal_constraints_total",
66 help: "How often the MFP-based literal-constraint detector succeeded, by call site.",
67 var_labels: ["case"],
68 )),
69 }
70 }
71
72 /// Records that a `literal_constraints` call at `case` produced a usable constraint.
73 pub fn inc_literal_constraints(&self, case: &str) {
74 self.literal_constraints.with_label_values(&[case]).inc();
75 }
76}
77
78/// The forms in which an operator's output is available.
79///
80/// These forms may include "raw", meaning as a streamed collection, but also any
81/// number of "arranged" representations.
82///
83/// Each arranged representation is described by a `(to_key, permutation, thinning)`
84/// triple, built by `permutation_for_arrangement`. `to_key` (length `K`) are the key
85/// expressions over a row. `permutation` (length `A`, the raw/unthinned arity) maps
86/// each row column to its position in the `(key, value)` concatenation. `thinning`
87/// (length `M`) lists the row columns that form the value, in value order, so a value
88/// datum at concatenation position `c >= K` came from row column `thinning[c - K]`.
89///
90/// This triple is unrelated to `KeyValRowMapping`'s `(to_key, to_val, to_row)` fields
91/// despite the visual resemblance: `permutation` here plays the role of
92/// `KeyValRowMapping::to_row`, and `thinning` plays the role of
93/// `KeyValRowMapping::to_val`. Do not assume the same field order.
94#[derive(
95 Clone,
96 Debug,
97 Default,
98 Deserialize,
99 Eq,
100 Ord,
101 PartialEq,
102 PartialOrd,
103 Serialize
104)]
105pub struct AvailableCollections {
106 /// Whether the collection exists in unarranged form.
107 pub raw: bool,
108 /// The list of available arrangements, each a `(to_key, permutation, thinning)`
109 /// triple. See the struct-level documentation for field semantics.
110 pub arranged: Vec<(Vec<LirScalarExpr>, Vec<usize>, Vec<usize>)>,
111}
112
113impl AvailableCollections {
114 /// Represent a collection that has no arrangements.
115 pub fn new_raw() -> Self {
116 Self {
117 raw: true,
118 arranged: Vec::new(),
119 }
120 }
121
122 /// Represent a collection that is arranged in the specified ways.
123 pub fn new_arranged(arranged: Vec<(Vec<LirScalarExpr>, Vec<usize>, Vec<usize>)>) -> Self {
124 assert!(
125 !arranged.is_empty(),
126 "Invariant violated: at least one collection must exist"
127 );
128 Self {
129 raw: false,
130 arranged,
131 }
132 }
133
134 /// Get some arrangement, if one exists.
135 pub fn arbitrary_arrangement(&self) -> Option<&(Vec<LirScalarExpr>, Vec<usize>, Vec<usize>)> {
136 assert!(
137 self.raw || !self.arranged.is_empty(),
138 "Invariant violated: at least one collection must exist"
139 );
140 self.arranged.get(0)
141 }
142}
143
144/// How to render the arrangements requested by an `ArrangeBy`.
145///
146/// Decided during LIR lowering and consumed by the renderer. The variant says what the
147/// renderer will do, not what it knows about the input.
148#[derive(
149 Clone,
150 Copy,
151 Debug,
152 Deserialize,
153 Eq,
154 Ord,
155 PartialEq,
156 PartialOrd,
157 Serialize
158)]
159pub enum ArrangementStrategy {
160 /// Form arrangements directly from the input collection.
161 Direct,
162 /// Insert temporal bucketing in front of the arrangement, to delay future-stamped
163 /// updates (e.g., from `mz_now()` MFPs) until their bucket boundary releases them.
164 TemporalBucketing,
165}
166
167impl std::fmt::Display for ArrangementStrategy {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match self {
170 ArrangementStrategy::Direct => write!(f, "Direct"),
171 ArrangementStrategy::TemporalBucketing => write!(f, "TemporalBucketing"),
172 }
173 }
174}
175
176/// An identifier for an LIR node.
177#[derive(
178 Clone,
179 Copy,
180 Debug,
181 Deserialize,
182 Eq,
183 Ord,
184 PartialEq,
185 PartialOrd,
186 Serialize,
187 Columnar
188)]
189pub struct LirId(u64);
190
191impl LirId {
192 fn as_u64(&self) -> u64 {
193 self.0
194 }
195}
196
197impl From<LirId> for u64 {
198 fn from(value: LirId) -> Self {
199 value.as_u64()
200 }
201}
202
203impl std::fmt::Display for LirId {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(f, "{}", self.0)
206 }
207}
208
209/// Version of the stable LIR serialization format.
210///
211/// Once a version has shipped, meaning a released Materialize durably stores
212/// pinned plans in it, bump this when the serialized representation of
213/// `PinnedDataflow` or anything it transitively contains changes, or when
214/// a scalar function's declared properties change. Whether the current
215/// version has shipped is recorded in [`LIR_VERSION_POLICY`]. Two snapshot
216/// tests key off it: the schema snapshot in `tests/lir_schema.rs` against
217/// `tests/snapshots/lir_v{LIR_VERSION}.json`, and the function property
218/// registry in `tests/func_registry.rs` against
219/// `tests/snapshots/func_registry.json` with a per-version digest in
220/// `tests/snapshots/func_registry_digests.json`.
221pub const LIR_VERSION: u64 = 1;
222
223/// What a change to the stable LIR format must do at the current
224/// [`LIR_VERSION`]. The snapshot tests print it when a snapshot goes stale.
225///
226/// This is the one place that records whether the current version has
227/// shipped. Once a released Materialize durably stores pinned plans, replace
228/// the text with the shipped policy: bump `LIR_VERSION` rather than rewriting
229/// the shipped version's snapshots, so the old schema stays available to
230/// migration tooling.
231pub const LIR_VERSION_POLICY: &str = "\
232The current LIR version has NOT shipped: no released Materialize stores pinned
233LIR plans yet, so nothing depends on the checked-in snapshots. Regenerate them
234in place and include the diff in your PR.
235
236Once pinned plans are durably stored, a change like this must instead bump
237LIR_VERSION in src/compute-types/src/plan.rs so it lands as a new version.
238See doc/developer/design/20260826_pinned_lir.md.";
239
240pub use constant_rows_serde::ConstantRows;
241
242/// Serializes `LirRelationNode::Constant`'s rows through the named
243/// [`ConstantRows`] mirror enum instead of std `Result`.
244///
245/// The stable LIR schema registry maps each container name to a single
246/// format, and `Result` would clash with the differently instantiated
247/// `Result` in `LirScalarExpr::Literal`. The mirror has the same variant
248/// order as `Result`, so the encoded bytes are unchanged.
249mod constant_rows_serde {
250 use mz_expr::{EvalError, StableEvalError, StableEvalErrorRef};
251 use mz_repr::{Diff, StableRow, Timestamp};
252 use serde::{Deserialize, Deserializer, Serialize, Serializer};
253
254 /// The serialized form of `LirRelationNode::Constant`'s rows.
255 #[derive(Debug, Serialize, Deserialize)]
256 pub enum ConstantRows {
257 /// See `Result::Ok`.
258 Ok(Vec<(StableRow, Timestamp, Diff)>),
259 /// See `Result::Err`.
260 Err(StableEvalError),
261 }
262
263 /// Borrowing mirror of [`ConstantRows`], to serialize without cloning.
264 #[derive(Serialize)]
265 #[serde(rename = "ConstantRows")]
266 enum ConstantRowsRef<'a> {
267 Ok(&'a Vec<(StableRow, Timestamp, Diff)>),
268 Err(StableEvalErrorRef<'a>),
269 }
270
271 pub fn serialize<S: Serializer>(
272 rows: &Result<Vec<(StableRow, Timestamp, Diff)>, EvalError>,
273 serializer: S,
274 ) -> Result<S::Ok, S::Error> {
275 let mirror = match rows {
276 Ok(rows) => ConstantRowsRef::Ok(rows),
277 Err(err) => ConstantRowsRef::Err(StableEvalErrorRef(err)),
278 };
279 mirror.serialize(serializer)
280 }
281
282 pub fn deserialize<'de, D: Deserializer<'de>>(
283 deserializer: D,
284 ) -> Result<Result<Vec<(StableRow, Timestamp, Diff)>, EvalError>, D::Error> {
285 Ok(match ConstantRows::deserialize(deserializer)? {
286 ConstantRows::Ok(rows) => Ok(rows),
287 ConstantRows::Err(err) => Err(err.0),
288 })
289 }
290}
291
292/// A rendering plan with as much conditional logic as possible removed.
293#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
294pub struct LirRelationExpr {
295 /// A dataflow-local identifier.
296 pub lir_id: LirId,
297 /// The underlying operator.
298 pub node: LirRelationNode,
299}
300
301/// The actual AST node of the `LirRelationExpr`.
302#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
303pub enum LirRelationNode {
304 /// A collection containing a pre-determined collection.
305 Constant {
306 /// Explicit update triples for the collection.
307 #[serde(with = "constant_rows_serde")]
308 rows: Result<Vec<(StableRow, Timestamp, Diff)>, EvalError>,
309 },
310 /// A reference to a bound collection.
311 ///
312 /// This is commonly either an external reference to an existing source or
313 /// maintained arrangement, or an internal reference to a `Let` identifier.
314 Get {
315 /// A global or local identifier naming the collection.
316 id: Id,
317 /// Arrangements that will be available.
318 ///
319 /// The collection will also be loaded if available, which it will
320 /// not be for imported data, but which it may be for locally defined
321 /// data.
322 // TODO: Be more explicit about whether a collection is available,
323 // although one can always produce it from an arrangement, and it
324 // seems generally advantageous to do that instead (to avoid cloning
325 // rows, by using `mfp` first on borrowed data).
326 keys: AvailableCollections,
327 /// The actions to take when introducing the collection.
328 plan: GetPlan,
329 },
330 /// Binds `value` to `id`, and then results in `body` with that binding.
331 ///
332 /// This stage has the effect of sharing `value` across multiple possible
333 /// uses in `body`, and is the only mechanism we have for sharing collection
334 /// information across parts of a dataflow.
335 ///
336 /// The binding is not available outside of `body`.
337 Let {
338 /// The local identifier to be used, available to `body` as `Id::Local(id)`.
339 id: LocalId,
340 /// The collection that should be bound to `id`.
341 value: Box<LirRelationExpr>,
342 /// The collection that results, which is allowed to contain `Get` stages
343 /// that reference `Id::Local(id)`.
344 body: Box<LirRelationExpr>,
345 },
346 /// Binds `values` to `ids`, evaluates them potentially recursively, and returns `body`.
347 ///
348 /// All bindings are available to all bindings, and to `body`.
349 /// The contents of each binding are initially empty, and then updated through a sequence
350 /// of iterations in which each binding is updated in sequence, from the most recent values
351 /// of all bindings.
352 LetRec {
353 /// The local identifiers to be used, available to `body` as `Id::Local(id)`.
354 ids: Vec<LocalId>,
355 /// The collection that should be bound to `id`.
356 values: Vec<LirRelationExpr>,
357 /// Maximum number of iterations. See further info on the MIR `LetRec`.
358 limits: Vec<Option<LetRecLimit>>,
359 /// The collection that results, which is allowed to contain `Get` stages
360 /// that reference `Id::Local(id)`.
361 body: Box<LirRelationExpr>,
362 },
363 /// Map, Filter, and Project operators.
364 ///
365 /// This stage contains work that we would ideally like to fuse to other plan
366 /// stages, but for practical reasons cannot. For example: threshold, topk,
367 /// and sometimes reduce stages are not able to absorb this operator.
368 Mfp {
369 /// The input collection.
370 input: Box<LirRelationExpr>,
371 /// Linear operator to apply to each record.
372 mfp: MfpPlan<LirScalarExpr>,
373 /// Whether the input is from an arrangement, and if so,
374 /// whether we can seek to a specific value therein
375 input_key_val: Option<(Vec<LirScalarExpr>, Option<StableRow>)>,
376 },
377 /// A variable number of output records for each input record.
378 ///
379 /// This stage is a bit of a catch-all for logic that does not easily fit in
380 /// map stages. This includes table valued functions, but also functions of
381 /// multiple arguments, and functions that modify the sign of updates.
382 ///
383 /// This stage allows a `MapFilterProject` operator to be fused to its output,
384 /// and this can be very important as otherwise the output of `func` is just
385 /// appended to the input record, for as many outputs as it has. This has the
386 /// unpleasant default behavior of repeating potentially large records that
387 /// are being unpacked, producing quadratic output in those cases. Instead,
388 /// in these cases use a `mfp` member that projects away these large fields.
389 FlatMap {
390 /// The particular arrangement of the input we expect to use,
391 /// if any
392 input_key: Option<Vec<LirScalarExpr>>,
393 /// The input collection.
394 input: Box<LirRelationExpr>,
395 /// Expressions that for each row prepare the arguments to `func`.
396 exprs: Vec<LirScalarExpr>,
397 /// The variable-record emitting function.
398 func: TableFunc,
399 /// Linear operator to apply to each record produced by `func`.
400 mfp_after: MfpPlan<LirScalarExpr>,
401 },
402 /// A multiway relational equijoin, with fused map, filter, and projection.
403 ///
404 /// This stage performs a multiway join among `inputs`, using the equality
405 /// constraints expressed in `plan`. The plan also describes the implementation
406 /// strategy we will use, and any pushed down per-record work.
407 Join {
408 /// An ordered list of inputs that will be joined.
409 inputs: Vec<LirRelationExpr>,
410 /// Detailed information about the implementation of the join.
411 ///
412 /// This includes information about the implementation strategy, but also
413 /// any map, filter, project work that we might follow the join with, but
414 /// potentially pushed down into the implementation of the join.
415 plan: JoinPlan,
416 },
417 /// Aggregation by key.
418 Reduce {
419 /// The particular arrangement of the input we expect to use,
420 /// if any
421 input_key: Option<Vec<LirScalarExpr>>,
422 /// The input collection.
423 input: Box<LirRelationExpr>,
424 /// A plan for changing input records into key, value pairs.
425 key_val_plan: KeyValPlan,
426 /// A plan for performing the reduce.
427 ///
428 /// The implementation of reduction has several different strategies based
429 /// on the properties of the reduction, and the input itself. Please check
430 /// out the documentation for this type for more detail.
431 plan: ReducePlan,
432 /// An MFP that must be applied to results. The projection part of this
433 /// MFP must preserve the key for the reduction; otherwise, the results
434 /// become undefined. Additionally, the MFP is guaranteed to be free from
435 /// temporal predicates so that it can be readily evaluated.
436 mfp_after: SafeMfpPlan<LirScalarExpr>,
437 /// Strategy for forming the internal input arrangement built by `Reduce`
438 /// (materialized via `key_val_plan`).
439 ///
440 /// Set by the lowering from the input's `has_future_updates` flag. The
441 /// renderer applies it to the keyed `(key, val)` stream feeding the
442 /// reduce. See `render_reduce` for the rationale on why this is
443 /// plumbed through `Reduce` rather than handled at the arrangement site.
444 ///
445 /// Note: unrelated to the hash buckets used by hierarchical reductions
446 /// (e.g. `ReducePlan::Hierarchical`'s `buckets`), which are an internal
447 /// sharding scheme for `min`/`max`-style aggregations. Here "bucketing"
448 /// refers exclusively to temporal (time-domain) bucketing of
449 /// future-stamped updates.
450 temporal_bucketing_strategy: ArrangementStrategy,
451 },
452 /// Key-based "Top K" operator, retaining the first K records in each group.
453 TopK {
454 /// The input collection.
455 input: Box<LirRelationExpr>,
456 /// A plan for performing the Top-K.
457 ///
458 /// The implementation of reduction has several different strategies based
459 /// on the properties of the reduction, and the input itself. Please check
460 /// out the documentation for this type for more detail.
461 top_k_plan: TopKPlan,
462 /// Strategy for bucketing the input collection ahead of the Top-K operator.
463 ///
464 /// Set by the lowering from the input's `has_future_updates` flag. The
465 /// renderer applies it to the per-row input stream at the top of
466 /// `render_topk`, covering all three `TopKPlan` arms uniformly. See
467 /// `LirRelationNode::Reduce::temporal_bucketing_strategy` for the underlying
468 /// convention.
469 temporal_bucketing_strategy: ArrangementStrategy,
470 },
471 /// Inverts the sign of each update.
472 Negate {
473 /// The input collection.
474 input: Box<LirRelationExpr>,
475 },
476 /// Filters records that accumulate negatively.
477 ///
478 /// Although the operator suppresses updates, it is a stateful operator taking
479 /// resources proportional to the number of records with non-zero accumulation.
480 Threshold {
481 /// The input collection.
482 input: Box<LirRelationExpr>,
483 /// A plan for performing the threshold.
484 ///
485 /// The implementation of reduction has several different strategies based
486 /// on the properties of the reduction, and the input itself. Please check
487 /// out the documentation for this type for more detail.
488 threshold_plan: ThresholdPlan,
489 },
490 /// Adds the contents of the input collections.
491 ///
492 /// Importantly, this is *multiset* union, so the multiplicities of records will
493 /// add. This is in contrast to *set* union, where the multiplicities would be
494 /// capped at one. A set union can be formed with `Union` followed by `Reduce`
495 /// implementing the "distinct" operator.
496 Union {
497 /// The input collections
498 inputs: Vec<LirRelationExpr>,
499 /// Whether to consolidate the output, e.g., cancel negated records.
500 consolidate_output: bool,
501 /// Per-input bucketing strategies. Lockstep with `inputs`: index `i` is the
502 /// strategy applied to `inputs[i]` before concatenation.
503 ///
504 /// Set by the lowering from each input's `has_future_updates` flag. Only
505 /// consolidating Unions (`consolidate_output: true`) carry non-`Direct`
506 /// entries, because bucketing only pays off ahead of a consolidating
507 /// downstream operator. See `LirRelationNode::Reduce::temporal_bucketing_strategy`
508 /// for the underlying convention.
509 temporal_bucketing_strategies: Vec<ArrangementStrategy>,
510 },
511 /// The `input` plan, but with additional arrangements.
512 ///
513 /// This operator does not change the logical contents of `input`, but ensures
514 /// that certain arrangements are available in the results. This operator can
515 /// be important for e.g. the `Join` stage which benefits from multiple arrangements
516 /// or to cap a `LirRelationExpr` so that indexes can be exported.
517 ArrangeBy {
518 /// The key that must be used to access the input.
519 input_key: Option<Vec<LirScalarExpr>>,
520 /// The input collection.
521 input: Box<LirRelationExpr>,
522 /// The MFP that must be applied to the input.
523 input_mfp: MfpPlan<LirScalarExpr>,
524 /// A list of arrangement keys, and possibly a raw collection,
525 /// that will be added to those of the input. Does not include
526 /// any other existing arrangements.
527 forms: AvailableCollections,
528 /// How the renderer should form the arrangements requested by `forms`.
529 strategy: ArrangementStrategy,
530 },
531}
532
533impl LirRelationNode {
534 /// Iterates through references to child expressions.
535 pub fn children(&self) -> impl Iterator<Item = &LirRelationExpr> {
536 let mut first = None;
537 let mut second = None;
538 let mut rest = None;
539 let mut last = None;
540
541 use LirRelationNode::*;
542 match self {
543 Constant { .. } | Get { .. } => (),
544 Let { value, body, .. } => {
545 first = Some(&**value);
546 second = Some(&**body);
547 }
548 LetRec { values, body, .. } => {
549 rest = Some(values);
550 last = Some(&**body);
551 }
552 Mfp { input, .. }
553 | FlatMap { input, .. }
554 | Reduce { input, .. }
555 | TopK { input, .. }
556 | Negate { input, .. }
557 | Threshold { input, .. }
558 | ArrangeBy { input, .. } => {
559 first = Some(&**input);
560 }
561 Join { inputs, .. } | Union { inputs, .. } => {
562 rest = Some(inputs);
563 }
564 }
565
566 first
567 .into_iter()
568 .chain(second)
569 .chain(rest.into_iter().flatten())
570 .chain(last)
571 }
572
573 /// Iterates through mutable references to child expressions.
574 pub fn children_mut(&mut self) -> impl Iterator<Item = &mut LirRelationExpr> {
575 let mut first = None;
576 let mut second = None;
577 let mut rest = None;
578 let mut last = None;
579
580 use LirRelationNode::*;
581 match self {
582 Constant { .. } | Get { .. } => (),
583 Let { value, body, .. } => {
584 first = Some(&mut **value);
585 second = Some(&mut **body);
586 }
587 LetRec { values, body, .. } => {
588 rest = Some(values);
589 last = Some(&mut **body);
590 }
591 Mfp { input, .. }
592 | FlatMap { input, .. }
593 | Reduce { input, .. }
594 | TopK { input, .. }
595 | Negate { input, .. }
596 | Threshold { input, .. }
597 | ArrangeBy { input, .. } => {
598 first = Some(&mut **input);
599 }
600 Join { inputs, .. } | Union { inputs, .. } => {
601 rest = Some(inputs);
602 }
603 }
604
605 first
606 .into_iter()
607 .chain(second)
608 .chain(rest.into_iter().flatten())
609 .chain(last)
610 }
611}
612
613impl LirRelationNode {
614 /// Attach an `lir_id` to a `LirRelationNode` to make a complete `LirRelationExpr`.
615 pub fn as_plan(self, lir_id: LirId) -> LirRelationExpr {
616 LirRelationExpr { lir_id, node: self }
617 }
618}
619
620impl LirRelationExpr {
621 /// Pretty-print this [LirRelationExpr] to a string.
622 pub fn pretty(&self) -> String {
623 let config = ExplainConfig::default();
624 self.debug_explain(&config, None)
625 }
626
627 /// Pretty-print this [LirRelationExpr] to a string using a custom
628 /// [ExplainConfig] and an optionally provided [ExprHumanizer].
629 /// This is intended for debugging and tests, not users.
630 pub fn debug_explain(
631 &self,
632 config: &ExplainConfig,
633 humanizer: Option<&dyn ExprHumanizer>,
634 ) -> String {
635 text_string_at(self, || PlanRenderingContext {
636 indent: Indent::default(),
637 humanizer: humanizer.unwrap_or(&DummyHumanizer),
638 annotations: BTreeMap::default(),
639 config,
640 ambiguous_ids: BTreeSet::default(),
641 })
642 }
643}
644
645/// How a `Get` stage will be rendered.
646#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
647pub enum GetPlan {
648 /// Simply pass input arrangements on to the next stage.
649 PassArrangements,
650 /// Using the supplied key, optionally seek the row, and apply the MFP.
651 Arrangement(
652 Vec<LirScalarExpr>,
653 Option<StableRow>,
654 MfpPlan<LirScalarExpr>,
655 ),
656 /// Scan the input collection (unarranged) and apply the MFP.
657 Collection(MfpPlan<LirScalarExpr>),
658}
659
660impl LirRelationExpr {
661 /// Convert the dataflow description into one that uses render plans.
662 #[mz_ore::instrument(
663 target = "optimizer",
664 level = "debug",
665 fields(path.segment = "finalize_dataflow")
666 )]
667 pub fn finalize_dataflow(
668 desc: DataflowDescription<OptimizedMirRelationExpr>,
669 features: &OptimizerFeatures,
670 metrics: Option<&LoweringMetrics>,
671 ) -> Result<LirDataflowDescription, String> {
672 fail::fail_point!("finalize_dataflow");
673
674 // First, we lower the dataflow description from MIR to LIR. Lowering
675 // also moves common parts of the MFPs pushed onto each source's reads into the source
676 // itself (see `Context::refine_source_mfps`).
677 let mut dataflow = Self::lower_dataflow(desc, features, metrics)?;
678
679 // Note: `consolidate_output` for `Union` and per-input
680 // `temporal_bucketing_strategies` are decided at lowering time (see the
681 // `Union` arm of `lower_mir_expr_stack_safe`). The pre-existing
682 // `refine_union_negate_consolidation` pass — which used to flip
683 // `consolidate_output` to `true` for Unions with a `Negate` child — has
684 // been folded into the lowering, since lowering is the only point where
685 // the bucketing decision (which depends on `has_future_updates`) is
686 // available.
687
688 if dataflow.is_single_time() {
689 // The relaxation of the `must_consolidate` flag performs an LIR-based
690 // analysis and transform under checked recursion. By a similar argument
691 // made in `from_mir`, we do not expect the recursion limit to be hit.
692 // However, if that happens, we propagate an error to the caller.
693 // To apply the transform, we first obtain monotonic source and index
694 // global IDs and add them to a `TransformConfig` instance.
695 let monotonic_ids = dataflow
696 .source_imports
697 .iter()
698 .filter_map(|(id, source_import)| source_import.monotonic.then_some(*id))
699 .chain(
700 dataflow
701 .index_imports
702 .iter()
703 .filter_map(|(_id, index_import)| {
704 if index_import.monotonic {
705 Some(index_import.desc.on_id)
706 } else {
707 None
708 }
709 }),
710 )
711 .collect::<BTreeSet<_>>();
712
713 let config = TransformConfig { monotonic_ids };
714 Self::refine_single_time_consolidation(&mut dataflow, &config)?;
715
716 // For non-recursive delta joins in single-time dataflows, only the delta path for the
717 // first relation produces updates: the other paths discard updates at the as-of, which
718 // is the only time present. We keep just that path and, where the first input's
719 // arrangement existed solely to seed it, drop the arrangement and consume the input as a
720 // raw collection. This requires rewriting the path's initial closure to address the raw
721 // row layout rather than the arranged `(key, value)` layout.
722 for build_desc in dataflow.objects_to_build.iter_mut() {
723 // Worklist of plan nodes. `LetRec` bodies are explored but `LetRec` values are not,
724 // which excludes recursive (WMR) joins from this transform.
725 let mut todo = vec![&mut build_desc.plan];
726 while let Some(expr) = todo.pop() {
727 match &mut expr.node {
728 // TODO: also handle binary differential joins, which can likewise shed a
729 // bespoke arrangement on their first input.
730 LirRelationNode::Join {
731 inputs,
732 plan: JoinPlan::Delta(plan),
733 } => {
734 // Only the first relation's path survives at a single time.
735 plan.path_plans.truncate(1);
736
737 let source_relation = plan.path_plans[0].source_relation;
738 // Replace the source input's bespoke arrangement with a raw collection,
739 // but only when the surviving path's source is fed by an `ArrangeBy`
740 // that exists solely to build that arrangement. A source backed directly
741 // by an arranged import has no `ArrangeBy` node here, so this guard skips
742 // it and the path keeps reading it arranged.
743 if let Some(source_key) = plan.path_plans[0].source_key.clone() {
744 if let LirRelationNode::ArrangeBy { forms, .. } =
745 &mut inputs[source_relation].node
746 {
747 // Drop arrangement forms other than the source key, which the
748 // remaining path no longer needs.
749 forms.arranged.retain(|(key, _, _)| key == &source_key);
750 if let Some((to_key, permutation, thinning)) =
751 forms.arranged.pop()
752 {
753 // Make the input a raw collection and unset the source key.
754 // Clearing every arrangement form is safe: `truncate(1)`
755 // already dropped the sibling paths that were the only other
756 // consumers, and this `ArrangeBy` is private to this join
757 // input. What remains is the input's raw collection.
758 forms.raw = true;
759 forms.arranged.clear();
760 plan.path_plans[0].source_key = None;
761
762 // The initial closure addresses the arranged `(key, value)`
763 // layout: columns `[0, K)` are key datums and columns
764 // `[K, K + M)` are the thinned value datums. We rewrite it to
765 // address the raw row instead.
766 //
767 // `to_key` (length `K`) are the key expressions over a row.
768 // `permutation` (length `A`, the raw arity) maps each row
769 // column to its position in the `(key, value)` concatenation.
770 // `thinning` (length `M`) lists the row columns that form the
771 // value.
772 let key_len = to_key.len();
773 let row_arity = permutation.len();
774 let closure = &mut plan.path_plans[0].initial_closure;
775
776 // Step 1: rewrite `ready_equivalences`, which reference the
777 // arranged layout. A key column becomes its defining
778 // expression. A value column becomes the row column it was
779 // projected from.
780 for class in closure.ready_equivalences.iter_mut() {
781 for expr in class.iter_mut() {
782 let mut todo = vec![expr];
783 while let Some(expr) = todo.pop() {
784 if let LirScalarExpr::Column(c, _) = expr {
785 if let Some(key_expr) = to_key.get(*c) {
786 *expr = key_expr.clone();
787 } else {
788 *c = thinning[*c - key_len];
789 }
790 } else {
791 todo.extend(expr.children_mut());
792 }
793 }
794 }
795 }
796
797 // Step 2: rewrite the `before` MFP. Starting from a raw row,
798 // materialize the key datums and project to the arranged
799 // `(key, value)` layout the original MFP expects, then apply
800 // it.
801 let (m, f, p) = closure.before.as_map_filter_project();
802 let mfp = MapFilterProject::new(row_arity)
803 .map(to_key)
804 .project(
805 (row_arity..row_arity + key_len).chain(thinning),
806 )
807 .map(m)
808 .filter(f)
809 .project(p);
810 closure.before =
811 mfp.into_plan().unwrap().into_nontemporal().unwrap();
812 }
813 }
814 }
815
816 todo.extend(inputs.iter_mut());
817 }
818 LirRelationNode::LetRec { body, .. } => {
819 todo.push(body);
820 }
821 x => {
822 todo.extend(x.children_mut());
823 }
824 }
825 }
826 }
827 }
828
829 soft_assert_eq_no_log!(dataflow.check_invariants(), Ok(()));
830
831 mz_repr::explain::trace_plan(&dataflow);
832
833 Ok(dataflow)
834 }
835
836 /// Lowers the dataflow description from MIR to LIR. To this end, the
837 /// method collects all available arrangements and based on this information
838 /// creates plans for every object to be built for the dataflow.
839 #[mz_ore::instrument(
840 target = "optimizer",
841 level = "debug",
842 fields(path.segment ="mir_to_lir")
843 )]
844 fn lower_dataflow(
845 desc: DataflowDescription<OptimizedMirRelationExpr>,
846 features: &OptimizerFeatures,
847 metrics: Option<&LoweringMetrics>,
848 ) -> Result<LirDataflowDescription, String> {
849 let context = lowering::Context::new(desc.debug_name.clone(), features, metrics);
850 let dataflow = context.lower(desc)?;
851
852 mz_repr::explain::trace_plan(&dataflow);
853
854 Ok(dataflow)
855 }
856
857 /// Refines the plans of objects to be built as part of a single-time `dataflow` to relax
858 /// the setting of the `must_consolidate` attribute of monotonic operators, if necessary,
859 /// whenever the input is deemed to be physically monotonic.
860 #[mz_ore::instrument(
861 target = "optimizer",
862 level = "debug",
863 fields(path.segment = "refine_single_time_consolidation")
864 )]
865 fn refine_single_time_consolidation(
866 dataflow: &mut LirDataflowDescription,
867 config: &TransformConfig,
868 ) -> Result<(), String> {
869 // We should only reach here if we have a one-shot SELECT query, i.e.,
870 // a single-time dataflow.
871 assert!(dataflow.is_single_time());
872
873 let transform = transform::RelaxMustConsolidate;
874 for build_desc in dataflow.objects_to_build.iter_mut() {
875 transform
876 .transform(config, &mut build_desc.plan)
877 .map_err(|_| "Maximum recursion limit error in consolidation relaxation.")?;
878 }
879 mz_repr::explain::trace_plan(dataflow);
880 Ok(())
881 }
882}
883
884impl CollectionPlan for LirRelationNode {
885 fn depends_on_into(&self, out: &mut BTreeSet<GlobalId>) {
886 match self {
887 LirRelationNode::Constant { rows: _ } => (),
888 LirRelationNode::Get {
889 id,
890 keys: _,
891 plan: _,
892 } => match id {
893 Id::Global(id) => {
894 out.insert(*id);
895 }
896 Id::Local(_) => (),
897 },
898 LirRelationNode::Let { id: _, value, body } => {
899 value.depends_on_into(out);
900 body.depends_on_into(out);
901 }
902 LirRelationNode::LetRec {
903 ids: _,
904 values,
905 limits: _,
906 body,
907 } => {
908 for value in values.iter() {
909 value.depends_on_into(out);
910 }
911 body.depends_on_into(out);
912 }
913 LirRelationNode::Join { inputs, plan: _ }
914 | LirRelationNode::Union {
915 inputs,
916 consolidate_output: _,
917 temporal_bucketing_strategies: _,
918 } => {
919 for input in inputs {
920 input.depends_on_into(out);
921 }
922 }
923 LirRelationNode::Mfp {
924 input,
925 mfp: _,
926 input_key_val: _,
927 }
928 | LirRelationNode::FlatMap {
929 input_key: _,
930 input,
931 exprs: _,
932 func: _,
933 mfp_after: _,
934 }
935 | LirRelationNode::ArrangeBy {
936 input_key: _,
937 input,
938 input_mfp: _,
939 forms: _,
940 strategy: _,
941 }
942 | LirRelationNode::Reduce {
943 input_key: _,
944 input,
945 key_val_plan: _,
946 plan: _,
947 mfp_after: _,
948 temporal_bucketing_strategy: _,
949 }
950 | LirRelationNode::TopK {
951 input,
952 top_k_plan: _,
953 temporal_bucketing_strategy: _,
954 }
955 | LirRelationNode::Negate { input }
956 | LirRelationNode::Threshold {
957 input,
958 threshold_plan: _,
959 } => {
960 input.depends_on_into(out);
961 }
962 }
963 }
964}
965
966impl CollectionPlan for LirRelationExpr {
967 fn depends_on_into(&self, out: &mut BTreeSet<GlobalId>) {
968 self.node.depends_on_into(out);
969 }
970}
971
972/// Returns bucket sizes, descending, suitable for hierarchical decomposition of an operator, based
973/// on the expected number of rows that will have the same group key.
974fn bucketing_of_expected_group_size(expected_group_size: Option<u64>) -> Vec<u64> {
975 // NOTE(vmarcos): The fan-in of 16 defined below is used in the tuning advice built-in view
976 // mz_introspection.mz_expected_group_size_advice.
977 let mut buckets = vec![];
978 let mut current = 16;
979
980 // Plan for 4B records in the expected case if the user didn't specify a group size.
981 let limit = expected_group_size.unwrap_or(4_000_000_000);
982
983 // Distribute buckets in powers of 16, so that we can strike a balance between how many inputs
984 // each layer gets from the preceding layer, while also limiting the number of layers.
985 while current < limit {
986 buckets.push(current);
987 current = current.saturating_mul(16);
988 }
989
990 buckets.reverse();
991 buckets
992}