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