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