mz_transform/dataflow.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//! Whole-dataflow optimization
11//!
12//! A dataflow may contain multiple views, each of which may only be
13//! optimized locally. However, information like demand and predicate
14//! pushdown can be applied across views once we understand the context
15//! in which the views will be executed.
16
17use std::collections::{BTreeMap, BTreeSet};
18
19use itertools::Itertools;
20use mz_compute_types::dataflows::{
21 BuildDesc, DataflowDescription, IndexImport, MirDataflowDescription,
22};
23use mz_expr::{
24 AccessStrategy, CollectionPlan, Id, JoinImplementation, LocalId, MapFilterProject,
25 MirRelationExpr, MirScalarExpr, RECURSION_LIMIT,
26};
27use mz_ore::stack::{CheckedRecursion, RecursionGuard, RecursionLimitError};
28use mz_ore::{assert_none, soft_assert_eq_or_log, soft_assert_or_log, soft_panic_or_log};
29use mz_repr::GlobalId;
30use mz_repr::explain::{DeltaJoinIndexUsageType, IndexUsageType, UsedIndexes};
31#[cfg(any(test, feature = "proptest"))]
32use proptest_derive::Arbitrary;
33use serde::{Deserialize, Serialize};
34
35use crate::monotonic::MonotonicFlag;
36use crate::notice::RawOptimizerNotice;
37use crate::{IndexOracle, Optimizer, TransformCtx, TransformError};
38
39/// Optimizes the implementation of each dataflow.
40///
41/// Inlines views, performs a full optimization pass including physical
42/// planning using the supplied indexes, propagates filtering and projection
43/// information to dataflow sources and lifts monotonicity information.
44#[mz_ore::instrument(
45 target = "optimizer",
46 level = "debug",
47 fields(path.segment ="global")
48)]
49pub fn optimize_dataflow(
50 dataflow: &mut MirDataflowDescription,
51 transform_ctx: &mut TransformCtx,
52 fast_path_optimizer: bool,
53) -> Result<(), TransformError> {
54 fail::fail_point!("optimize_dataflow");
55
56 // Inline views that are used in only one other view.
57 inline_views(dataflow)?;
58
59 if fast_path_optimizer {
60 optimize_dataflow_relations(
61 dataflow,
62 &Optimizer::fast_path_optimizer(transform_ctx),
63 transform_ctx,
64 )?;
65 } else {
66 // Logical optimization pass after view inlining
67 optimize_dataflow_relations(
68 dataflow,
69 #[allow(deprecated)]
70 &Optimizer::logical_optimizer(transform_ctx),
71 transform_ctx,
72 )?;
73
74 optimize_dataflow_filters(dataflow)?;
75 // TODO: when the linear operator contract ensures that propagated
76 // predicates are always applied, projections and filters can be removed
77 // from where they come from. Once projections and filters can be removed,
78 // TODO: it would be useful for demand to be optimized after filters
79 // that way demand only includes the columns that are still necessary after
80 // the filters are applied.
81 optimize_dataflow_demand(dataflow)?;
82
83 // A smaller logical optimization pass after projections and filters are
84 // pushed down across views.
85 optimize_dataflow_relations(
86 dataflow,
87 &Optimizer::logical_cleanup_pass(transform_ctx, false),
88 transform_ctx,
89 )?;
90
91 // Physical optimization pass
92 optimize_dataflow_relations(
93 dataflow,
94 &Optimizer::physical_optimizer(transform_ctx),
95 transform_ctx,
96 )?;
97
98 optimize_dataflow_monotonic(dataflow, transform_ctx)?;
99 }
100
101 prune_and_annotate_dataflow_index_imports(
102 dataflow,
103 transform_ctx.indexes,
104 transform_ctx.df_meta,
105 )?;
106
107 prune_dataflow_source_imports(dataflow);
108
109 // Warning: If you want to add a transform call here, consider it very carefully whether it
110 // could accidentally invalidate information that we already derived above in
111 // `optimize_dataflow_monotonic`, `prune_and_annotate_dataflow_index_imports`, or
112 // `prune_dataflow_source_imports`. A transform here that drops the last reference to an import
113 // puts back exactly the discrepancy the two prunes just removed.
114
115 mz_repr::explain::trace_plan(dataflow);
116
117 Ok(())
118}
119
120/// Inline views used in one other view, and in no exported objects.
121#[mz_ore::instrument(
122 target = "optimizer",
123 level = "debug",
124 fields(path.segment = "inline_views")
125)]
126fn inline_views(dataflow: &mut MirDataflowDescription) -> Result<(), TransformError> {
127 // We cannot inline anything whose `BuildDesc::id` appears in either the
128 // `index_exports` or `sink_exports` of `dataflow`, because we lose our
129 // ability to name it.
130
131 // A view can / should be in-lined in another view if it is only used by
132 // one subsequent view. If there are two distinct views that have not
133 // themselves been merged, then too bad and it doesn't get inlined.
134
135 // Starting from the *last* object to build, walk backwards and inline
136 // any view that is neither referenced by a `index_exports` nor
137 // `sink_exports` nor more than two remaining objects to build.
138
139 for index in (0..dataflow.objects_to_build.len()).rev() {
140 // Capture the name used by others to reference this view.
141 let global_id = dataflow.objects_to_build[index].id;
142 // Determine if any exports directly reference this view.
143 let mut occurs_in_export = false;
144 for (_gid, sink_desc) in dataflow.sink_exports.iter() {
145 if sink_desc.from == global_id {
146 occurs_in_export = true;
147 }
148 }
149 for (_, (index_desc, _)) in dataflow.index_exports.iter() {
150 if index_desc.on_id == global_id {
151 occurs_in_export = true;
152 }
153 }
154 // Count the number of subsequent views that reference this view.
155 let mut occurrences_in_later_views = Vec::new();
156 for other in (index + 1)..dataflow.objects_to_build.len() {
157 if dataflow.objects_to_build[other]
158 .plan
159 .depends_on()
160 .contains(&global_id)
161 {
162 occurrences_in_later_views.push(other);
163 }
164 }
165 // Inline if the view is referenced in one view and no exports.
166 if !occurs_in_export && occurrences_in_later_views.len() == 1 {
167 let other = occurrences_in_later_views[0];
168 // We can remove this view and insert it in the later view,
169 // but are not able to relocate the later view `other`.
170
171 // When splicing in the `index` view, we need to create disjoint
172 // identifiers for the Let's `body` and `value`, as well as a new
173 // identifier for the binding itself. Following `NormalizeLets`, we
174 // go with the binding first, then the value, then the body.
175 let mut id_gen = crate::IdGen::default();
176 let new_local = LocalId::new(id_gen.allocate_id());
177 // Use the same `id_gen` to assign new identifiers to `index`.
178 crate::normalize_lets::renumber_bindings(
179 dataflow.objects_to_build[index].plan.as_inner_mut(),
180 &mut id_gen,
181 )?;
182 // Assign new identifiers to the other relation.
183 crate::normalize_lets::renumber_bindings(
184 dataflow.objects_to_build[other].plan.as_inner_mut(),
185 &mut id_gen,
186 )?;
187 // Install the `new_local` name wherever `global_id` was used.
188 dataflow.objects_to_build[other]
189 .plan
190 .as_inner_mut()
191 .visit_pre_mut(|expr| {
192 if let MirRelationExpr::Get { id, .. } = expr {
193 if id == &Id::Global(global_id) {
194 *id = Id::Local(new_local);
195 }
196 }
197 });
198
199 // With identifiers rewritten, we can replace `other` with
200 // a `MirRelationExpr::Let` binding, whose value is `index` and
201 // whose body is `other`.
202 let body = dataflow.objects_to_build[other]
203 .plan
204 .as_inner_mut()
205 .take_dangerous();
206 let value = dataflow.objects_to_build[index]
207 .plan
208 .as_inner_mut()
209 .take_dangerous();
210 *dataflow.objects_to_build[other].plan.as_inner_mut() = MirRelationExpr::Let {
211 id: new_local,
212 value: Box::new(value),
213 body: Box::new(body),
214 };
215 dataflow.objects_to_build.remove(index);
216 }
217 }
218
219 mz_repr::explain::trace_plan(dataflow);
220
221 Ok(())
222}
223
224/// Performs either the logical or the physical optimization pass on the
225/// dataflow using the supplied set of indexes.
226#[mz_ore::instrument(
227 target = "optimizer",
228 level = "debug",
229 fields(path.segment = optimizer.name)
230)]
231fn optimize_dataflow_relations(
232 dataflow: &mut MirDataflowDescription,
233 optimizer: &Optimizer,
234 ctx: &mut TransformCtx,
235) -> Result<(), TransformError> {
236 // Re-optimize each dataflow
237 for object in dataflow.objects_to_build.iter_mut() {
238 // Re-run all optimizations on the composite views.
239 ctx.set_global_id(object.id);
240 optimizer.transform(object.plan.as_inner_mut(), ctx)?;
241 ctx.reset_global_id();
242 }
243
244 mz_repr::explain::trace_plan(dataflow);
245
246 Ok(())
247}
248
249/// Pushes demand information from published outputs to dataflow inputs,
250/// projecting away unnecessary columns.
251///
252/// Dataflows that exist for the sake of generating plan explanations do not
253/// have published outputs. In this case, we push demand information from views
254/// not depended on by other views to dataflow inputs.
255#[mz_ore::instrument(
256 target = "optimizer",
257 level = "debug",
258 fields(path.segment ="demand")
259)]
260fn optimize_dataflow_demand(dataflow: &mut MirDataflowDescription) -> Result<(), TransformError> {
261 // Maps id -> union of known columns demanded from the source/view with the
262 // corresponding id.
263 let mut demand = BTreeMap::new();
264
265 if dataflow.index_exports.is_empty() && dataflow.sink_exports.is_empty() {
266 // In the absence of any exports, just demand all columns from views
267 // that are not depended on by another view, which is currently the last
268 // object in `objects_to_build`.
269
270 // A MirDataflowDescription without exports is currently created in the context of
271 // EXPLAIN outputs. This ensures that the output has all the columns of
272 // the original explainee.
273 if let Some(build_desc) = dataflow.objects_to_build.iter_mut().rev().next() {
274 demand
275 .entry(Id::Global(build_desc.id))
276 .or_insert_with(BTreeSet::new)
277 .extend(0..build_desc.plan.as_inner_mut().arity());
278 }
279 } else {
280 // Demand all columns of inputs to sinks.
281 for (_id, sink) in dataflow.sink_exports.iter() {
282 let input_id = sink.from;
283 demand
284 .entry(Id::Global(input_id))
285 .or_insert_with(BTreeSet::new)
286 .extend(0..dataflow.arity_of(&input_id));
287 }
288
289 // Demand all columns of inputs to exported indexes.
290 for (_id, (desc, _typ)) in dataflow.index_exports.iter() {
291 let input_id = desc.on_id;
292 demand
293 .entry(Id::Global(input_id))
294 .or_insert_with(BTreeSet::new)
295 .extend(0..dataflow.arity_of(&input_id));
296 }
297 }
298
299 optimize_dataflow_demand_inner(
300 dataflow
301 .objects_to_build
302 .iter_mut()
303 .rev()
304 .map(|build_desc| (Id::Global(build_desc.id), build_desc.plan.as_inner_mut())),
305 &mut demand,
306 )?;
307
308 mz_repr::explain::trace_plan(dataflow);
309
310 Ok(())
311}
312
313/// Pushes demand through views in `view_sequence` in order, removing
314/// columns not demanded.
315///
316/// This method is made public for the sake of testing.
317/// TODO: make this private once we allow multiple exports per dataflow.
318pub fn optimize_dataflow_demand_inner<'a, I>(
319 view_sequence: I,
320 demand: &mut BTreeMap<Id, BTreeSet<usize>>,
321) -> Result<(), TransformError>
322where
323 I: Iterator<Item = (Id, &'a mut MirRelationExpr)>,
324{
325 // Maps id -> The projection that was pushed down on the view with the
326 // corresponding id.
327 let mut applied_projection = BTreeMap::new();
328 // Collect the mutable references to views after pushing projection down
329 // in order to run cleanup actions on them in a second loop.
330 let mut view_refs = Vec::new();
331 let projection_pushdown = crate::movement::ProjectionPushdown::default();
332 for (id, view) in view_sequence {
333 if let Some(columns) = demand.get(&id) {
334 let projection_pushed_down = columns.iter().map(|c| *c).collect();
335 // Push down the projection consisting of the entries of `columns`
336 // in increasing order.
337 projection_pushdown.action(view, &projection_pushed_down, demand)?;
338 let new_type = view.typ();
339 applied_projection.insert(id, (projection_pushed_down, new_type));
340 }
341 view_refs.push(view);
342 }
343
344 for view in view_refs {
345 // Update `Get` nodes to reflect any columns that have been projected away.
346 projection_pushdown.update_projection_around_get(view, &applied_projection);
347 }
348
349 Ok(())
350}
351
352/// Pushes predicate to dataflow inputs.
353#[mz_ore::instrument(
354 target = "optimizer",
355 level = "debug",
356 fields(path.segment ="filters")
357)]
358fn optimize_dataflow_filters(dataflow: &mut MirDataflowDescription) -> Result<(), TransformError> {
359 // Contains id -> predicates map, describing those predicates that
360 // can (but need not) be applied to the collection named by `id`.
361 let mut predicates = BTreeMap::<Id, BTreeSet<mz_expr::MirScalarExpr>>::new();
362
363 // Propagate predicate information from outputs to inputs.
364 optimize_dataflow_filters_inner(
365 dataflow
366 .objects_to_build
367 .iter_mut()
368 .rev()
369 .map(|build_desc| (Id::Global(build_desc.id), build_desc.plan.as_inner_mut())),
370 &mut predicates,
371 )?;
372
373 // Push predicate information into the SourceDesc.
374 for (source_id, source_import) in dataflow.source_imports.iter_mut() {
375 let source = &mut source_import.desc;
376 if let Some(list) = predicates.remove(&Id::Global(*source_id)) {
377 if !list.is_empty() {
378 // Canonicalize the order of predicates, for stable plans.
379 let mut list = list.into_iter().collect::<Vec<_>>();
380 list.sort();
381 // Install no-op predicate information if none exists.
382 if source.arguments.operators.is_none() {
383 source.arguments.operators = Some(MapFilterProject::new(source.typ.arity()));
384 }
385 // Add any predicates that can be pushed to the source.
386 if let Some(operator) = source.arguments.operators.take() {
387 source.arguments.operators = Some(operator.filter(list));
388 source.arguments.operators.as_mut().map(|x| x.optimize());
389 }
390 }
391 }
392 }
393
394 mz_repr::explain::trace_plan(dataflow);
395
396 Ok(())
397}
398
399/// Pushes filters down through views in `view_sequence` in order.
400///
401/// This method is made public for the sake of testing.
402/// TODO: make this private once we allow multiple exports per dataflow.
403pub fn optimize_dataflow_filters_inner<'a, I>(
404 view_iter: I,
405 predicates: &mut BTreeMap<Id, BTreeSet<mz_expr::MirScalarExpr>>,
406) -> Result<(), TransformError>
407where
408 I: Iterator<Item = (Id, &'a mut MirRelationExpr)>,
409{
410 let transform = crate::predicate_pushdown::PredicatePushdown::default();
411 for (id, view) in view_iter {
412 if let Some(list) = predicates.get(&id).clone() {
413 if !list.is_empty() {
414 *view = view.take_dangerous().filter(list.iter().cloned());
415 }
416 }
417 transform.action(view, predicates)?;
418 }
419 Ok(())
420}
421
422/// Propagates information about monotonic inputs through operators,
423/// using [`mz_repr::optimize::OptimizerFeatures`] from `ctx` for [`crate::analysis::Analysis`].
424#[mz_ore::instrument(
425 target = "optimizer",
426 level = "debug",
427 fields(path.segment ="monotonic")
428)]
429pub fn optimize_dataflow_monotonic(
430 dataflow: &mut MirDataflowDescription,
431 ctx: &mut TransformCtx,
432) -> Result<(), TransformError> {
433 let mut monotonic_ids = BTreeSet::new();
434 for (source_id, source_import) in dataflow.source_imports.iter() {
435 if source_import.monotonic {
436 monotonic_ids.insert(source_id.clone());
437 }
438 }
439 for (
440 _index_id,
441 IndexImport {
442 desc: index_desc,
443 monotonic,
444 ..
445 },
446 ) in dataflow.index_imports.iter()
447 {
448 if *monotonic {
449 monotonic_ids.insert(index_desc.on_id.clone());
450 }
451 }
452
453 let monotonic_flag = MonotonicFlag::default();
454
455 for build_desc in dataflow.objects_to_build.iter_mut() {
456 monotonic_flag.transform(build_desc.plan.as_inner_mut(), ctx, &monotonic_ids)?;
457 }
458
459 mz_repr::explain::trace_plan(dataflow);
460
461 Ok(())
462}
463
464/// Determine whether we require snapshots from our durable source imports.
465/// (For example, these can often be skipped for simple subscribe queries.)
466pub fn optimize_dataflow_snapshot(
467 dataflow: &mut MirDataflowDescription,
468) -> Result<(), TransformError> {
469 // For every global id, true iff we need a snapshot for that global ID.
470 // This is computed bottom-up: subscribes may or may not require a snapshot from their inputs,
471 // index exports definitely do, and objects-to-build require a snapshot from their inputs if
472 // either they need to provide a snapshot as output or they may need snapshots internally, eg. to
473 // compute a join.
474 let mut downstream_requires_snapshot = BTreeMap::new();
475
476 for (_id, export) in &dataflow.sink_exports {
477 *downstream_requires_snapshot
478 .entry(Id::Global(export.from))
479 .or_default() |= export.with_snapshot;
480 }
481 for (_id, (export, _typ)) in &dataflow.index_exports {
482 *downstream_requires_snapshot
483 .entry(Id::Global(export.on_id))
484 .or_default() |= true;
485 }
486 for BuildDesc { id: _, plan } in dataflow.objects_to_build.iter().rev() {
487 // For now, we treat all intermediate nodes as potentially requiring a snapshot.
488 // Walk the AST, marking anything depended on by a compute object as snapshot-required.
489 let mut todo = vec![(true, &plan.0)];
490 while let Some((requires_snapshot, expr)) = todo.pop() {
491 match expr {
492 MirRelationExpr::Get { id, .. } => {
493 *downstream_requires_snapshot.entry(*id).or_default() |= requires_snapshot;
494 }
495 other => {
496 todo.extend(other.children().rev().map(|c| (true, c)));
497 }
498 }
499 }
500 }
501 for (id, import) in &mut dataflow.source_imports {
502 let with_snapshot = downstream_requires_snapshot
503 .entry(Id::Global(*id))
504 .or_default();
505
506 // As above, fetch the snapshot if there are any transformations on the raw source data.
507 // (And we'll always need to check for things like temporal filters, since those allow
508 // snapshot data to affect diffs at times past the as-of.)
509 *with_snapshot |= import.desc.arguments.operators.is_some();
510
511 import.with_snapshot = *with_snapshot;
512 }
513 for (_id, import) in &mut dataflow.index_imports {
514 let with_snapshot = downstream_requires_snapshot
515 .entry(Id::Global(import.desc.on_id))
516 .or_default();
517
518 import.with_snapshot = *with_snapshot;
519 }
520
521 Ok(())
522}
523
524/// Restricts the sources imported by `dataflow` to only the ones its exports read.
525///
526/// The counterpart to [`prune_and_annotate_dataflow_index_imports`] for source imports. Imports are
527/// collected before the global pipeline runs, from the plans as they were written, so a transform
528/// can drop the last `Get` of one, for instance by folding a selection to a constant.
529///
530/// An import that survives that is not free. Every worker builds a `persist_source` for it and
531/// decodes a shard into a stream nobody consumes, and the controller takes a read hold that pins
532/// the collection's `since` for as long as the dataflow lives. Both are read off the import list
533/// directly, so pruning is what reclaims them.
534///
535/// A third consumer, the wall-clock dependence a dataflow reports, is the one whose wrong answer
536/// does real damage: it earns a dataflow whose exports can never change again an expiration, which
537/// pins their output frontier at the expiration time rather than letting it reach the empty
538/// antichain, so nothing downstream learns the collection is final.
539/// `ComputeController::determine_time_dependence` derives that from the read set rather than from
540/// the import list, so it does not depend on this pass having run. `create_dataflow` also reports a
541/// list this pass left loose.
542///
543/// The input plans should be normalized with `NormalizeLets`, for the same reason
544/// [`prune_and_annotate_dataflow_index_imports`] wants them to be: an unused `Let` binding can
545/// otherwise keep alive a `Get` that nothing reads.
546#[mz_ore::instrument(
547 target = "optimizer",
548 level = "debug",
549 fields(path.segment = "source_imports")
550)]
551fn prune_dataflow_source_imports(dataflow: &mut MirDataflowDescription) {
552 // NOTE: A description with no exports has no answer to "what do the exports read", and pruning
553 // everything is the wrong one. `EXPLAIN` builds a peek description without its index export,
554 // see the conditional `export_index` in `mz_adapter::optimize::peek`. Such a description is
555 // explained and then dropped, never installed, so leaving its import list alone costs nothing.
556 if dataflow.index_exports.is_empty() && dataflow.sink_exports.is_empty() {
557 return;
558 }
559
560 let used = dataflow.used_import_ids();
561 dataflow.source_imports.retain(|id, _| used.contains(id));
562}
563
564/// Restricts the indexes imported by `dataflow` to only the ones it needs.
565/// It also adds to the `DataflowMetainfo` how each index will be used.
566/// It also annotates global `Get`s with whether they will be reads from Persist or an index, plus
567/// their index usage types.
568///
569/// The input `dataflow` should import all indexes belonging to all views/sources/tables it
570/// references.
571///
572/// The input plans should be normalized with `NormalizeLets`! Otherwise, we might find dangling
573/// `ArrangeBy`s at the top of unused Let bindings.
574#[mz_ore::instrument(
575 target = "optimizer",
576 level = "debug",
577 fields(path.segment = "index_imports")
578)]
579fn prune_and_annotate_dataflow_index_imports(
580 dataflow: &mut MirDataflowDescription,
581 indexes: &dyn IndexOracle,
582 dataflow_metainfo: &mut DataflowMetainfo,
583) -> Result<(), TransformError> {
584 // Preparation.
585 // Let's save the unique keys of the sources. This will inform which indexes to choose for full
586 // scans. (We can't get this info from `source_imports`, because `source_imports` only has those
587 // sources that are not getting an indexed read.)
588 let mut source_keys = BTreeMap::new();
589 for build_desc in dataflow.objects_to_build.iter() {
590 build_desc
591 .plan
592 .as_inner()
593 .visit_pre(|expr: &MirRelationExpr| match expr {
594 MirRelationExpr::Get {
595 id: Id::Global(global_id),
596 typ,
597 ..
598 } => {
599 source_keys.entry(*global_id).or_insert_with(|| {
600 typ.keys
601 .iter()
602 .map(|key| {
603 key.iter()
604 // Convert the Vec<usize> key to Vec<MirScalarExpr>, so that
605 // later we can more easily compare index keys to these keys.
606 .map(|col_idx| MirScalarExpr::column(*col_idx))
607 .collect()
608 })
609 .collect()
610 });
611 }
612 _ => {}
613 });
614 }
615
616 // This will be a mapping of
617 // (ids used by exports and objects to build) ->
618 // (arrangement keys and usage types on that id that have been requested)
619 let mut index_reqs_by_id = BTreeMap::new();
620
621 // Go through the MIR plans of `objects_to_build` and collect which arrangements are requested
622 // for which we also have an available index.
623 for build_desc in dataflow.objects_to_build.iter_mut() {
624 CollectIndexRequests::new(&source_keys, indexes, &mut index_reqs_by_id)
625 .collect_index_reqs(build_desc.plan.as_inner_mut())?;
626 }
627
628 // Collect index usages by `sink_exports`.
629 // A sink export sometimes wants to directly use an imported index. I know of one case where
630 // this happens: The dataflow for a SUBSCRIBE on an indexed view won't have any
631 // `objects_to_build`, but will want to directly read from the index and write to a sink.
632 for (_sink_id, sink_desc) in dataflow.sink_exports.iter() {
633 // First, let's see if there exists an index on the id that the sink wants. If not, there is
634 // nothing we can do here.
635 if let Some((idx_id, arbitrary_idx_key)) = indexes.indexes_on(sink_desc.from).next() {
636 // If yes, then we'll add a request of _some_ index: If we already collected an index
637 // request on this id, then use that, otherwise use the above arbitrarily picked index.
638 let requested_idxs = index_reqs_by_id
639 .entry(sink_desc.from)
640 .or_insert_with(Vec::new);
641 if let Some((already_req_idx_id, already_req_key, _)) = requested_idxs.get(0) {
642 requested_idxs.push((
643 *already_req_idx_id,
644 already_req_key.clone(),
645 IndexUsageType::SinkExport,
646 ));
647 } else {
648 requested_idxs.push((
649 idx_id,
650 arbitrary_idx_key.to_owned(),
651 IndexUsageType::SinkExport,
652 ));
653 }
654 }
655 }
656
657 // Collect index usages by `index_exports`.
658 for (_id, (index_desc, _)) in dataflow.index_exports.iter() {
659 // First, let's see if there exists an index on the id that the exported index is on. If
660 // not, there is nothing we can do here.
661 if let Some((idx_id, arbitrary_index_key)) = indexes.indexes_on(index_desc.on_id).next() {
662 // If yes, then we'll add an index request of some index: If we already collected an
663 // index request on this id, then use that, otherwise use the above arbitrarily picked
664 // index.
665 let requested_idxs = index_reqs_by_id
666 .entry(index_desc.on_id)
667 .or_insert_with(Vec::new);
668 if let Some((already_req_idx_id, already_req_key, _)) = requested_idxs.get(0) {
669 requested_idxs.push((
670 *already_req_idx_id,
671 already_req_key.clone(),
672 IndexUsageType::IndexExport,
673 ));
674 } else {
675 // This is surprising: Actually, an index creation dataflow always has a plan in
676 // `objects_to_build` that will have a Get of the object that the index is on (see
677 // `DataflowDescription::export_index`). Therefore, we should have already requested
678 // an index usage when seeing that Get in `CollectIndexRequests`.
679 soft_panic_or_log!(
680 "We are seeing an index export on an id that's not mentioned in `objects_to_build`"
681 );
682 requested_idxs.push((
683 idx_id,
684 arbitrary_index_key.to_owned(),
685 IndexUsageType::IndexExport,
686 ));
687 }
688 }
689 }
690
691 // By now, `index_reqs_by_id` has all ids that we think might benefit from having an index on.
692 // Moreover, for each of these ids, if any index exists on it, then we should have already
693 // picked one. If not, then we have a bug somewhere. In that case, do a soft panic, and add an
694 // Unknown usage, picking an arbitrary index.
695 for (id, index_reqs) in index_reqs_by_id.iter_mut() {
696 if index_reqs.is_empty() {
697 // Try to pick an arbitrary index to be fully scanned.
698 if let Some((idx_id, key)) = indexes.indexes_on(*id).next() {
699 soft_panic_or_log!(
700 "prune_and_annotate_dataflow_index_imports didn't find any index for an id, even though one exists
701id: {}, key: {:?}",
702 id,
703 key
704 );
705 index_reqs.push((idx_id, key.to_owned(), IndexUsageType::Unknown));
706 }
707 }
708 }
709
710 // Adjust FullScans to not introduce a new index dependency if there is also some non-FullScan
711 // request on the same id.
712 // `full_scan_changes` saves the changes that we do: Each (Get id, index id) entry indicates
713 // that if a Get has that id, then any full scan index accesses on it should be changed to use
714 // the indicated index id.
715 let mut full_scan_changes = BTreeMap::new();
716 for (get_id, index_reqs) in index_reqs_by_id.iter_mut() {
717 // Let's choose a non-FullScan access (if exists).
718 if let Some((picked_idx, picked_idx_key)) = choose_index(
719 &source_keys,
720 get_id,
721 &index_reqs
722 .iter()
723 .filter_map(|(idx_id, key, usage_type)| match usage_type {
724 IndexUsageType::FullScan => None,
725 _ => Some((*idx_id, key.clone())),
726 })
727 .collect_vec(),
728 ) {
729 // Found a non-FullScan access. Modify all FullScans to use the same index as that one.
730 for (idx_id, key, usage_type) in index_reqs {
731 match usage_type {
732 IndexUsageType::FullScan => {
733 full_scan_changes.insert(get_id, picked_idx);
734 *idx_id = picked_idx;
735 key.clone_from(&picked_idx_key);
736 }
737 _ => {}
738 }
739 }
740 }
741 }
742 // Apply the above full scan changes to also the Gets.
743 for build_desc in dataflow.objects_to_build.iter_mut() {
744 build_desc
745 .plan
746 .as_inner_mut()
747 .visit_pre_mut(|expr: &mut MirRelationExpr| {
748 match expr {
749 MirRelationExpr::Get {
750 id: Id::Global(global_id),
751 typ: _,
752 access_strategy: persist_or_index,
753 } => {
754 if let Some(new_idx_id) = full_scan_changes.get(global_id) {
755 match persist_or_index {
756 AccessStrategy::UnknownOrLocal => {
757 // Should have been already filled by `collect_index_reqs`.
758 unreachable!()
759 }
760 AccessStrategy::Persist => {
761 // We already know that it's an indexed access.
762 unreachable!()
763 }
764 AccessStrategy::SameDataflow => {
765 // We have not added such annotations yet.
766 unreachable!()
767 }
768 AccessStrategy::Index(accesses) => {
769 for (idx_id, usage_type) in accesses {
770 if matches!(usage_type, IndexUsageType::FullScan) {
771 *idx_id = *new_idx_id;
772 }
773 }
774 }
775 }
776 }
777 }
778 _ => {}
779 }
780 });
781 }
782
783 // Annotate index imports by their usage types
784 dataflow_metainfo.index_usage_types = BTreeMap::new();
785 for (
786 index_id,
787 IndexImport {
788 desc: index_desc,
789 typ: _,
790 monotonic: _,
791 with_snapshot: _,
792 },
793 ) in dataflow.index_imports.iter_mut()
794 {
795 // A sanity check that we are not importing an index that we are also exporting.
796 assert!(
797 !dataflow
798 .index_exports
799 .iter()
800 .map(|(exported_index_id, _)| exported_index_id)
801 .any(|exported_index_id| exported_index_id == index_id)
802 );
803
804 let mut new_usage_types = Vec::new();
805 // Let's see whether something has requested an index on this object that this imported
806 // index is on.
807 if let Some(index_reqs) = index_reqs_by_id.get(&index_desc.on_id) {
808 for (req_idx_id, req_key, req_usage_type) in index_reqs {
809 if req_idx_id == index_id {
810 soft_assert_eq_or_log!(*req_key, index_desc.key);
811 new_usage_types.push(req_usage_type.clone());
812 }
813 }
814 }
815 if !new_usage_types.is_empty() {
816 dataflow_metainfo
817 .index_usage_types
818 .insert(*index_id, new_usage_types);
819 }
820 }
821
822 // Prune index imports to only those that are used
823 dataflow
824 .index_imports
825 .retain(|id, _index_import| dataflow_metainfo.index_usage_types.contains_key(id));
826
827 // Determine AccessStrategy::SameDataflow accesses. These were classified as
828 // AccessStrategy::Persist inside collect_index_reqs, so now we check these, and if the id is of
829 // a collection that we are building ourselves, then we adjust the access strategy.
830 let mut objects_to_build_ids = BTreeSet::new();
831 for BuildDesc { id, plan: _ } in dataflow.objects_to_build.iter() {
832 objects_to_build_ids.insert(id.clone());
833 }
834 for build_desc in dataflow.objects_to_build.iter_mut() {
835 build_desc
836 .plan
837 .as_inner_mut()
838 .visit_pre_mut(|expr: &mut MirRelationExpr| match expr {
839 MirRelationExpr::Get {
840 id: Id::Global(global_id),
841 typ: _,
842 access_strategy,
843 } => match access_strategy {
844 AccessStrategy::Persist => {
845 if objects_to_build_ids.contains(global_id) {
846 *access_strategy = AccessStrategy::SameDataflow;
847 }
848 }
849 _ => {}
850 },
851 _ => {}
852 });
853 }
854
855 // A sanity check that all Get annotations indicate indexes that are present in `index_imports`.
856 for build_desc in dataflow.objects_to_build.iter() {
857 build_desc
858 .plan
859 .as_inner()
860 .visit_pre(|expr: &MirRelationExpr| match expr {
861 MirRelationExpr::Get {
862 id: Id::Global(_),
863 typ: _,
864 access_strategy: AccessStrategy::Index(accesses),
865 } => {
866 for (idx_id, _) in accesses {
867 soft_assert_or_log!(
868 dataflow.index_imports.contains_key(idx_id),
869 "Dangling Get index annotation"
870 );
871 }
872 }
873 _ => {}
874 });
875 }
876
877 mz_repr::explain::trace_plan(dataflow);
878
879 Ok(())
880}
881
882/// Pick an index from a given Vec of index keys.
883///
884/// Currently, we pick as follows:
885/// - If there is an index on a unique key, then we pick that. (It might be better distributed, and
886/// is less likely to get dropped than other indexes.)
887/// - Otherwise, we pick an arbitrary index.
888///
889/// TODO: There are various edge cases where a better choice would be possible:
890/// - Some indexes might be less skewed than others. (Although, picking a unique key tries to
891/// capture this already.)
892/// - Some indexes might have an error, while others don't.
893/// <https://github.com/MaterializeInc/database-issues/issues/4455>
894/// - Some indexes might have more extra data in their keys (because of being on more complicated
895/// expressions than just column references), which won't be used in a full scan.
896fn choose_index(
897 source_keys: &BTreeMap<GlobalId, BTreeSet<Vec<MirScalarExpr>>>,
898 id: &GlobalId,
899 indexes: &Vec<(GlobalId, Vec<MirScalarExpr>)>,
900) -> Option<(GlobalId, Vec<MirScalarExpr>)> {
901 match source_keys.get(id) {
902 None => indexes.iter().next().cloned(), // pick an arbitrary index
903 Some(coll_keys) => match indexes
904 .iter()
905 .find(|(_idx_id, key)| coll_keys.contains(&*key))
906 {
907 Some((idx_id, key)) => Some((*idx_id, key.clone())),
908 None => indexes.iter().next().cloned(), // pick an arbitrary index
909 },
910 }
911}
912
913#[derive(Debug)]
914struct CollectIndexRequests<'a> {
915 /// We were told about these unique keys on sources.
916 source_keys: &'a BTreeMap<GlobalId, BTreeSet<Vec<MirScalarExpr>>>,
917 /// We were told about these indexes being available.
918 indexes_available: &'a dyn IndexOracle,
919 /// We'll be collecting index requests here.
920 index_reqs_by_id:
921 &'a mut BTreeMap<GlobalId, Vec<(GlobalId, Vec<MirScalarExpr>, IndexUsageType)>>,
922 /// As we recurse down a MirRelationExpr, we'll need to keep track of the context of the
923 /// current node (see docs on `IndexUsageContext` about what context we keep).
924 /// Moreover, we need to propagate this context from cte uses to cte definitions.
925 /// `context_across_lets` will keep track of the contexts that reached each use of a LocalId
926 /// added together.
927 context_across_lets: BTreeMap<LocalId, Vec<IndexUsageContext>>,
928 recursion_guard: RecursionGuard,
929}
930
931impl<'a> CheckedRecursion for CollectIndexRequests<'a> {
932 fn recursion_guard(&self) -> &RecursionGuard {
933 &self.recursion_guard
934 }
935}
936
937impl<'a> CollectIndexRequests<'a> {
938 fn new(
939 source_keys: &'a BTreeMap<GlobalId, BTreeSet<Vec<MirScalarExpr>>>,
940 indexes_available: &'a dyn IndexOracle,
941 index_reqs_by_id: &'a mut BTreeMap<
942 GlobalId,
943 Vec<(GlobalId, Vec<MirScalarExpr>, IndexUsageType)>,
944 >,
945 ) -> CollectIndexRequests<'a> {
946 CollectIndexRequests {
947 source_keys,
948 indexes_available,
949 index_reqs_by_id,
950 context_across_lets: BTreeMap::new(),
951 recursion_guard: RecursionGuard::with_limit(RECURSION_LIMIT),
952 }
953 }
954
955 pub fn collect_index_reqs(
956 &mut self,
957 expr: &mut MirRelationExpr,
958 ) -> Result<(), RecursionLimitError> {
959 assert!(self.context_across_lets.is_empty());
960 self.collect_index_reqs_inner(
961 expr,
962 &IndexUsageContext::from_usage_type(IndexUsageType::PlanRootNoArrangement),
963 )?;
964 assert!(self.context_across_lets.is_empty());
965 // Sanity check that we don't have any `DeltaJoinIndexUsageType::Unknown` remaining.
966 for (_id, index_reqs) in self.index_reqs_by_id.iter() {
967 for (_, _, index_usage_type) in index_reqs {
968 soft_assert_or_log!(
969 !matches!(
970 index_usage_type,
971 IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Unknown)
972 ),
973 "Delta join Unknown index usage remained"
974 );
975 }
976 }
977 Ok(())
978 }
979
980 fn collect_index_reqs_inner(
981 &mut self,
982 expr: &mut MirRelationExpr,
983 contexts: &Vec<IndexUsageContext>,
984 ) -> Result<(), RecursionLimitError> {
985 self.checked_recur_mut(|this| {
986 // If an index exists on `on_id`, this function picks an index to be fully scanned.
987 let pick_index_for_full_scan = |on_id: &GlobalId| {
988 // Note that the choice we make here might be modified later at the
989 // "Adjust FullScans to not introduce a new index dependency".
990 choose_index(
991 this.source_keys,
992 on_id,
993 &this
994 .indexes_available
995 .indexes_on(*on_id)
996 .map(|(idx_id, key)| (idx_id, key.iter().cloned().collect_vec()))
997 .collect_vec(),
998 )
999 };
1000
1001 // See comment on `IndexUsageContext`.
1002 Ok(match expr {
1003 MirRelationExpr::Join {
1004 inputs,
1005 implementation,
1006 ..
1007 } => {
1008 match implementation {
1009 JoinImplementation::Differential(..) => {
1010 for input in inputs {
1011 this.collect_index_reqs_inner(
1012 input,
1013 &IndexUsageContext::from_usage_type(
1014 IndexUsageType::DifferentialJoin,
1015 ),
1016 )?;
1017 }
1018 }
1019 JoinImplementation::DeltaQuery(..) => {
1020 // For Delta joins, the first input is special, see
1021 // https://github.com/MaterializeInc/database-issues/issues/2115
1022 this.collect_index_reqs_inner(
1023 &mut inputs[0],
1024 &IndexUsageContext::from_usage_type(IndexUsageType::DeltaJoin(
1025 DeltaJoinIndexUsageType::Unknown,
1026 )),
1027 )?;
1028 for input in &mut inputs[1..] {
1029 this.collect_index_reqs_inner(
1030 input,
1031 &IndexUsageContext::from_usage_type(IndexUsageType::DeltaJoin(
1032 DeltaJoinIndexUsageType::Lookup,
1033 )),
1034 )?;
1035 }
1036 }
1037 JoinImplementation::IndexedFilter(_coll_id, idx_id, ..) => {
1038 for input in inputs {
1039 this.collect_index_reqs_inner(
1040 input,
1041 &IndexUsageContext::from_usage_type(IndexUsageType::Lookup(
1042 *idx_id,
1043 )),
1044 )?;
1045 }
1046 }
1047 JoinImplementation::Unimplemented => {
1048 soft_panic_or_log!(
1049 "CollectIndexRequests encountered an Unimplemented join"
1050 );
1051 }
1052 }
1053 }
1054 MirRelationExpr::ArrangeBy { input, keys } => {
1055 let ctx = &IndexUsageContext::add_keys(contexts, keys);
1056 this.collect_index_reqs_inner(input, ctx)?;
1057 }
1058 MirRelationExpr::Get {
1059 id: Id::Global(global_id),
1060 access_strategy: persist_or_index,
1061 ..
1062 } => {
1063 this.index_reqs_by_id
1064 .entry(*global_id)
1065 .or_insert_with(Vec::new);
1066 // If the context is empty, it means we didn't see an operator that would
1067 // specifically want to use an index for this Get. However, let's still try to
1068 // find an index for a full scan.
1069 let mut try_full_scan = contexts.is_empty();
1070 let mut index_accesses = Vec::new();
1071 for context in contexts {
1072 match &context.requested_keys {
1073 None => {
1074 // We have some index usage context, but didn't see an `ArrangeBy`.
1075 try_full_scan = true;
1076 match context.usage_type {
1077 IndexUsageType::FullScan
1078 | IndexUsageType::SinkExport
1079 | IndexUsageType::IndexExport => {
1080 // Not possible, because these don't go through
1081 // IndexUsageContext at all.
1082 unreachable!()
1083 }
1084 // You can find more info on why the following join cases
1085 // shouldn't happen in comments of the Join lowering to LIR.
1086 IndexUsageType::Lookup(_) => soft_panic_or_log!(
1087 "CollectIndexRequests encountered \
1088 an IndexedFilter join without an ArrangeBy"
1089 ),
1090 IndexUsageType::DifferentialJoin => soft_panic_or_log!(
1091 "CollectIndexRequests encountered \
1092 a Differential join without an ArrangeBy"
1093 ),
1094 IndexUsageType::DeltaJoin(_) => soft_panic_or_log!(
1095 "CollectIndexRequests encountered \
1096 a Delta join without an ArrangeBy"
1097 ),
1098 IndexUsageType::PlanRootNoArrangement => {
1099 // This is ok: the entire plan is a `Get`, with not even an
1100 // `ArrangeBy`. Note that if an index exists, the usage will
1101 // be saved as `FullScan` (NOT as `PlanRootNoArrangement`),
1102 // because we are going into the `try_full_scan` if.
1103 }
1104 IndexUsageType::FastPathLimit => {
1105 // These are created much later, not even inside
1106 // `prune_and_annotate_dataflow_index_imports`.
1107 unreachable!()
1108 }
1109 IndexUsageType::DanglingArrangeBy => {
1110 // Not possible, because we create `DanglingArrangeBy`
1111 // only when we see an `ArrangeBy`.
1112 unreachable!()
1113 }
1114 IndexUsageType::Unknown => {
1115 // These are added only after `CollectIndexRequests` has run.
1116 unreachable!()
1117 }
1118 }
1119 }
1120 Some(requested_keys) => {
1121 for requested_key in requested_keys {
1122 match this.indexes_available.indexes_on(*global_id).find(
1123 |(available_idx_id, available_key)| {
1124 match context.usage_type {
1125 IndexUsageType::Lookup(req_idx_id) => {
1126 // `LiteralConstraints` already picked an index
1127 // by id. Let's use that one.
1128 assert!(
1129 !(available_idx_id == &req_idx_id
1130 && available_key != &requested_key)
1131 );
1132 available_idx_id == &req_idx_id
1133 }
1134 _ => available_key == &requested_key,
1135 }
1136 },
1137 ) {
1138 Some((idx_id, key)) => {
1139 let usage = context.usage_type.clone();
1140 this.index_reqs_by_id
1141 .get_mut(global_id)
1142 .unwrap()
1143 .push((idx_id, key.to_owned(), usage.clone()));
1144 index_accesses.push((idx_id, usage));
1145 }
1146 None => {
1147 // If there is a key requested for which we don't have an
1148 // index, then we might still be able to do a full scan of a
1149 // differently keyed index.
1150 try_full_scan = true;
1151 }
1152 }
1153 }
1154 if requested_keys.is_empty() {
1155 // It's a bit weird if an MIR ArrangeBy is not requesting any
1156 // key, but let's try a full scan in that case anyhow.
1157 try_full_scan = true;
1158 }
1159 }
1160 }
1161 }
1162 if try_full_scan {
1163 // Keep in mind that when having 2 contexts coming from 2 uses of a Let,
1164 // this code can't distinguish between the case when there is 1 ArrangeBy at the
1165 // top of the Let, or when the 2 uses each have an `ArrangeBy`. In both cases,
1166 // we'll add only 1 full scan, which would be wrong in the latter case. However,
1167 // the latter case can't currently happen until we do
1168 // https://github.com/MaterializeInc/database-issues/issues/6363
1169 // Also note that currently we are deduplicating index usage types when
1170 // printing index usages in EXPLAIN.
1171 if let Some((idx_id, key)) = pick_index_for_full_scan(global_id) {
1172 this.index_reqs_by_id.get_mut(global_id).unwrap().push((
1173 idx_id,
1174 key.to_owned(),
1175 IndexUsageType::FullScan,
1176 ));
1177 index_accesses.push((idx_id, IndexUsageType::FullScan));
1178 }
1179 }
1180 if index_accesses.is_empty() {
1181 *persist_or_index = AccessStrategy::Persist;
1182 } else {
1183 *persist_or_index = AccessStrategy::Index(index_accesses);
1184 }
1185 }
1186 MirRelationExpr::Get {
1187 id: Id::Local(local_id),
1188 ..
1189 } => {
1190 // Add the current context to the vector of contexts of `local_id`.
1191 // (The unwrap is safe, because the Let and LetRec cases start with inserting an
1192 // empty entry.)
1193 this.context_across_lets
1194 .get_mut(local_id)
1195 .unwrap()
1196 .extend(contexts.iter().cloned());
1197 // No recursive call here, because Get has no inputs.
1198 }
1199 MirRelationExpr::Let { id, value, body } => {
1200 let shadowed_context = this.context_across_lets.insert(id.clone(), Vec::new());
1201 // No shadowing in MIR
1202 assert_none!(shadowed_context);
1203 // We go backwards: Recurse on the body and then the value.
1204 this.collect_index_reqs_inner(body, contexts)?;
1205 // The above call filled in the entry for `id` in `context_across_lets` (if it
1206 // was referenced). Anyhow, at least an empty entry should exist, because we started
1207 // above with inserting it.
1208 this.collect_index_reqs_inner(value, &this.context_across_lets[id].clone())?;
1209 // Clean up the id from the saved contexts.
1210 this.context_across_lets.remove(id);
1211 }
1212 MirRelationExpr::LetRec {
1213 ids,
1214 values,
1215 limits: _,
1216 body,
1217 } => {
1218 for id in ids.iter() {
1219 let shadowed_context =
1220 this.context_across_lets.insert(id.clone(), Vec::new());
1221 // No shadowing in MIR
1222 assert_none!(shadowed_context);
1223 }
1224 // We go backwards: Recurse on the body first.
1225 this.collect_index_reqs_inner(body, contexts)?;
1226 // Reset the contexts of the ids (of the current LetRec), because an arrangement
1227 // from a value can't be used in the body.
1228 for id in ids.iter() {
1229 *this.context_across_lets.get_mut(id).unwrap() = Vec::new();
1230 }
1231 // Recurse on the values in reverse order.
1232 // Note that we do only one pass, i.e., we won't see context through a Get that
1233 // refers to the previous iteration. But this is ok, because we can't reuse
1234 // arrangements across iterations anyway.
1235 for (id, value) in ids.iter().rev().zip_eq(values.iter_mut().rev()) {
1236 this.collect_index_reqs_inner(
1237 value,
1238 &this.context_across_lets[id].clone(),
1239 )?;
1240 }
1241 // Clean up the ids from the saved contexts.
1242 for id in ids {
1243 this.context_across_lets.remove(id);
1244 }
1245 }
1246 _ => {
1247 // Nothing interesting at this node, recurse with the empty context (regardless of
1248 // what context we got from above).
1249 let empty_context = Vec::new();
1250 for child in expr.children_mut() {
1251 this.collect_index_reqs_inner(child, &empty_context)?;
1252 }
1253 }
1254 })
1255 })
1256 }
1257}
1258
1259/// This struct will save info about parent nodes as we are descending down a `MirRelationExpr`.
1260/// We always start with filling in `usage_type` when we see an operation that uses an arrangement,
1261/// and then we fill in `requested_keys` when we see an `ArrangeBy`. So, the pattern that we are
1262/// looking for is
1263/// ```text
1264/// <operation that uses an index>
1265/// ArrangeBy <requested_keys>
1266/// Get <global_id>
1267/// ```
1268/// When we reach a `Get` to a global id, we access this context struct to see if the rest of the
1269/// pattern is present above the `Get`.
1270///
1271/// Note that we usually put this struct in a Vec, because we track context across local let
1272/// bindings, which means that a node can have multiple parents.
1273#[derive(Debug, Clone)]
1274struct IndexUsageContext {
1275 usage_type: IndexUsageType,
1276 requested_keys: Option<BTreeSet<Vec<MirScalarExpr>>>,
1277}
1278
1279impl IndexUsageContext {
1280 pub fn from_usage_type(usage_type: IndexUsageType) -> Vec<Self> {
1281 vec![IndexUsageContext {
1282 usage_type,
1283 requested_keys: None,
1284 }]
1285 }
1286
1287 // Add the keys of an ArrangeBy into the contexts.
1288 // Soft_panics if haven't already seen something that indicates what the index will be used for.
1289 pub fn add_keys(
1290 old_contexts: &Vec<IndexUsageContext>,
1291 keys_to_add: &Vec<Vec<MirScalarExpr>>,
1292 ) -> Vec<IndexUsageContext> {
1293 let old_contexts = if old_contexts.is_empty() {
1294 // No join above us, and we are not at the root. Why does this ArrangeBy even exist?
1295 soft_panic_or_log!("CollectIndexRequests encountered a dangling ArrangeBy");
1296 // Anyhow, let's create a context with a `DanglingArrangeBy` index usage, so that we
1297 // have a place to note down the requested keys below.
1298 IndexUsageContext::from_usage_type(IndexUsageType::DanglingArrangeBy)
1299 } else {
1300 old_contexts.clone()
1301 };
1302 old_contexts
1303 .into_iter()
1304 .flat_map(|old_context| {
1305 if !matches!(
1306 old_context.usage_type,
1307 IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Unknown)
1308 ) {
1309 // If it's not an unknown delta join usage, then we simply note down the new
1310 // keys into `requested_keys`.
1311 let mut context = old_context.clone();
1312 if context.requested_keys.is_none() {
1313 context.requested_keys = Some(BTreeSet::new());
1314 }
1315 context
1316 .requested_keys
1317 .as_mut()
1318 .unwrap()
1319 .extend(keys_to_add.iter().cloned());
1320 Some(context).into_iter().chain(None)
1321 } else {
1322 // If it's an unknown delta join usage, then we need to figure out whether this
1323 // is a full scan or a lookup.
1324 //
1325 // `source_key` in `DeltaPathPlan` determines which arrangement we are going to
1326 // scan when starting the rendering of a delta path. This is the one for which
1327 // we want a `DeltaJoinIndexUsageType::FirstInputFullScan`.
1328 //
1329 // However, `DeltaPathPlan` is an LIR concept, and here we need to figure out
1330 // the `source_key` based on the MIR plan. We do this by doing the same as
1331 // `DeltaJoinPlan::create_from`: choose the smallest key (by `Ord`).
1332 let source_key = keys_to_add
1333 .iter()
1334 .min()
1335 .expect("ArrangeBy below a delta join has at least one key");
1336 let full_scan_context = IndexUsageContext {
1337 requested_keys: Some(BTreeSet::from([source_key.clone()])),
1338 usage_type: IndexUsageType::DeltaJoin(
1339 DeltaJoinIndexUsageType::FirstInputFullScan,
1340 ),
1341 };
1342 let lookup_keys = keys_to_add
1343 .into_iter()
1344 .filter(|key| *key != source_key)
1345 .cloned()
1346 .collect_vec();
1347 if lookup_keys.is_empty() {
1348 Some(full_scan_context).into_iter().chain(None)
1349 } else {
1350 let lookup_context = IndexUsageContext {
1351 requested_keys: Some(lookup_keys.into_iter().collect()),
1352 usage_type: IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Lookup),
1353 };
1354 Some(full_scan_context)
1355 .into_iter()
1356 .chain(Some(lookup_context))
1357 }
1358 }
1359 })
1360 .collect()
1361 }
1362}
1363
1364/// Extra information about the dataflow. This is not going to be shipped, but has to be processed
1365/// in other ways, e.g., showing notices to the user, or saving meta-information to the catalog.
1366#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1367#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
1368pub struct DataflowMetainfo<Notice = RawOptimizerNotice> {
1369 /// Notices that the optimizer wants to show to users.
1370 /// For pushing a new element, use [`Self::push_optimizer_notice_dedup`].
1371 pub optimizer_notices: Vec<Notice>,
1372 /// What kind of operation (full scan, lookup, ...) will access each index. Computed by
1373 /// `prune_and_annotate_dataflow_index_imports`.
1374 pub index_usage_types: BTreeMap<GlobalId, Vec<IndexUsageType>>,
1375}
1376
1377impl<Notice> Default for DataflowMetainfo<Notice> {
1378 fn default() -> Self {
1379 DataflowMetainfo {
1380 optimizer_notices: Vec::new(),
1381 index_usage_types: BTreeMap::new(),
1382 }
1383 }
1384}
1385
1386impl<Notice> DataflowMetainfo<Notice> {
1387 /// Create a [`UsedIndexes`] instance by resolving each `id` in the
1388 /// `index_ids` iterator against an entry expected to exist in the
1389 /// [`DataflowMetainfo::index_usage_types`].
1390 pub fn used_indexes<T, O>(&self, df_desc: &DataflowDescription<T, (), O>) -> UsedIndexes {
1391 UsedIndexes::new(
1392 df_desc
1393 .index_imports
1394 .iter()
1395 .map(|(id, _)| {
1396 let entry = self.index_usage_types.get(id).cloned();
1397 // If an entry does not exist, mark the usage type for this
1398 // index as `Unknown`.
1399 //
1400 // This should never happen if this method is called after
1401 // running `prune_and_annotate_dataflow_index_imports` on
1402 // the dataflow (this happens at the end of the
1403 // `optimize_dataflow` call).
1404 let index_usage_type = entry.unwrap_or_else(|| vec![IndexUsageType::Unknown]);
1405
1406 (*id, index_usage_type)
1407 })
1408 .collect(),
1409 )
1410 }
1411}
1412
1413impl DataflowMetainfo<RawOptimizerNotice> {
1414 /// Pushes a [`RawOptimizerNotice`] into [`Self::optimizer_notices`], but
1415 /// only if the exact same notice is not already present.
1416 pub fn push_optimizer_notice_dedup<T>(&mut self, notice: T)
1417 where
1418 T: Into<RawOptimizerNotice>,
1419 {
1420 let notice = notice.into();
1421 if !self.optimizer_notices.contains(¬ice) {
1422 self.optimizer_notices.push(notice);
1423 }
1424 }
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429 use mz_compute_types::sinks::{
1430 ComputeSinkConnection, ComputeSinkDesc, SubscribeSinkConnection,
1431 };
1432 use mz_expr::OptimizedMirRelationExpr;
1433 use mz_repr::{RelationDesc, ReprRelationType, ReprScalarType, SqlRelationType};
1434
1435 use super::*;
1436
1437 const READ: GlobalId = GlobalId::User(1);
1438 const UNREAD: GlobalId = GlobalId::User(2);
1439 const VIEW: GlobalId = GlobalId::Transient(1);
1440 const SINK: GlobalId = GlobalId::Transient(2);
1441
1442 fn typ() -> ReprRelationType {
1443 ReprRelationType::new(vec![ReprScalarType::Int64.nullable(false)])
1444 }
1445
1446 /// A dataflow importing `READ` and `UNREAD` and building `VIEW` from `plan`. It has no exports
1447 /// until `export_subscribe` adds one.
1448 fn dataflow(plan: MirRelationExpr) -> MirDataflowDescription {
1449 let mut df = MirDataflowDescription::new("test".to_string());
1450 df.import_source(READ, SqlRelationType::from_repr(&typ()), false);
1451 df.import_source(UNREAD, SqlRelationType::from_repr(&typ()), false);
1452 df.objects_to_build.push(BuildDesc {
1453 id: VIEW,
1454 plan: OptimizedMirRelationExpr::declare_optimized(plan),
1455 });
1456 df
1457 }
1458
1459 fn export_subscribe(df: &mut MirDataflowDescription) {
1460 df.export_sink(
1461 SINK,
1462 ComputeSinkDesc {
1463 from: VIEW,
1464 from_desc: RelationDesc::new(SqlRelationType::from_repr(&typ()), ["c"]),
1465 connection: ComputeSinkConnection::Subscribe(SubscribeSinkConnection {
1466 output: Vec::new(),
1467 }),
1468 with_snapshot: true,
1469 up_to: Default::default(),
1470 non_null_assertions: Vec::new(),
1471 refresh_schedule: None,
1472 from_arrangement: None,
1473 },
1474 );
1475 }
1476
1477 fn get(id: GlobalId) -> MirRelationExpr {
1478 MirRelationExpr::Get {
1479 id: Id::Global(id),
1480 typ: typ(),
1481 access_strategy: AccessStrategy::Persist,
1482 }
1483 }
1484
1485 fn constant() -> MirRelationExpr {
1486 MirRelationExpr::Constant {
1487 rows: Ok(Vec::new()),
1488 typ: typ(),
1489 }
1490 }
1491
1492 #[mz_ore::test]
1493 fn prune_drops_the_import_no_export_reads() {
1494 let mut df = dataflow(get(READ));
1495 export_subscribe(&mut df);
1496
1497 prune_dataflow_source_imports(&mut df);
1498
1499 assert_eq!(df.imported_source_ids().collect::<Vec<_>>(), vec![READ]);
1500 }
1501
1502 /// The shape this prune exists for: the optimizer folded the export to a constant, so neither
1503 /// import is read any more even though both are still listed.
1504 #[mz_ore::test]
1505 fn prune_drops_every_import_of_a_constant_export() {
1506 let mut df = dataflow(constant());
1507 export_subscribe(&mut df);
1508
1509 prune_dataflow_source_imports(&mut df);
1510
1511 assert_eq!(df.imported_source_ids().count(), 0);
1512 }
1513
1514 /// A description with no exports is one `EXPLAIN` builds and never installs. Pruning it against
1515 /// an empty set of exports would strip every import, so the prune leaves it alone.
1516 #[mz_ore::test]
1517 fn prune_leaves_an_export_less_description_alone() {
1518 let mut df = dataflow(get(READ));
1519
1520 prune_dataflow_source_imports(&mut df);
1521
1522 assert_eq!(
1523 df.imported_source_ids().collect::<Vec<_>>(),
1524 vec![READ, UNREAD]
1525 );
1526 }
1527}