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