mz_transform/notice/
index_too_wide_for_literal_constraints.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//! Hosts [`IndexTooWideForLiteralConstraints`].
11
12use std::collections::BTreeSet;
13use std::fmt;
14
15use itertools::zip_eq;
16use mz_expr::MirScalarExpr;
17use mz_expr::explain::{HumanizedNotice, HumanizerMode};
18use mz_ore::str::separated;
19use mz_repr::GlobalId;
20use mz_repr::Row;
21use mz_repr::explain::ExprHumanizer;
22
23use crate::notice::{ActionKind, OptimizerNoticeApi};
24
25/// An index could be used for some literal constraints if the index included only a subset of its
26/// columns.
27#[derive(Clone, Debug, Eq, PartialEq, Hash)]
28pub struct IndexTooWideForLiteralConstraints {
29    /// The id of the index.
30    pub index_id: GlobalId,
31    /// The key of the index.
32    pub index_key: Vec<MirScalarExpr>,
33    /// A subset of the index keys. If the index were only on these keys, then it could have been
34    /// used for a lookup.
35    pub usable_subset: Vec<MirScalarExpr>,
36    /// Literal values that we would have looked up in the index, if it were on `usable_subset`.
37    pub literal_values: Vec<Row>,
38    /// The id of the object that the index is on.
39    pub index_on_id: GlobalId,
40    /// Our recommendation for what key should a new index have. Note that this might include more
41    /// columns than `usable_subset`.
42    pub recommended_key: Vec<MirScalarExpr>,
43}
44
45impl OptimizerNoticeApi for IndexTooWideForLiteralConstraints {
46    fn dependencies(&self) -> BTreeSet<GlobalId> {
47        BTreeSet::from([self.index_id, self.index_on_id])
48    }
49
50    fn fmt_message(
51        &self,
52        f: &mut fmt::Formatter<'_>,
53        humanizer: &dyn ExprHumanizer,
54        redacted: bool,
55    ) -> fmt::Result {
56        let col_names = humanizer.column_names_for_id(self.index_on_id);
57        let col_names = col_names.as_ref();
58
59        let index_name = humanizer
60            .humanize_id(self.index_id)
61            .unwrap_or_else(|| self.index_id.to_string());
62        let index_on_id_name = humanizer
63            .humanize_id_unqualified(self.index_on_id)
64            .unwrap_or_else(|| self.index_on_id.to_string());
65
66        let mode = HumanizedNotice::new(redacted);
67
68        let index_key = separated(", ", mode.seq(&self.index_key, col_names));
69        write!(
70            f,
71            "Index {index_name} on {index_on_id_name}({index_key}) \
72             is too wide to use for literal equalities "
73        )?;
74
75        write!(f, "`")?;
76        {
77            if self.usable_subset.len() == 1 {
78                let exprs = mode.expr(&self.usable_subset[0], col_names);
79                let lits = self
80                    .literal_values
81                    .iter()
82                    .map(|l| l.unpack_first())
83                    .collect::<Vec<_>>();
84                let mut lits = mode.seq(&lits, col_names);
85                if self.literal_values.len() == 1 {
86                    write!(f, "{} = {}", exprs, lits.next().unwrap())?;
87                } else {
88                    write!(f, "{} IN ({})", exprs, separated(", ", lits))?;
89                }
90            } else {
91                if self.literal_values.len() == 1 {
92                    let exprs = mode.seq(&self.usable_subset, col_names);
93                    let lits = self.literal_values[0].unpack();
94                    let lits = mode.seq(&lits, col_names);
95                    let eqs = zip_eq(exprs, lits).map(|(expr, lit)| format!("{} = {}", expr, lit));
96                    write!(f, "{}", separated(" AND ", eqs))?;
97                } else {
98                    let exprs = mode.seq(&self.usable_subset, col_names);
99                    let lits = mode.seq(&self.literal_values, col_names);
100                    write!(
101                        f,
102                        "({}) IN ({})",
103                        separated(", ", exprs),
104                        separated(", ", lits)
105                    )?;
106                }
107            };
108        }
109        write!(f, "`.")
110    }
111
112    fn fmt_hint(
113        &self,
114        f: &mut fmt::Formatter<'_>,
115        humanizer: &dyn ExprHumanizer,
116        redacted: bool,
117    ) -> fmt::Result {
118        let col_names = humanizer.column_names_for_id(self.index_on_id);
119
120        let mode = HumanizedNotice::new(redacted);
121
122        let recommended_key = mode.seq(&self.recommended_key, col_names.as_ref());
123        let recommended_key = separated(", ", recommended_key);
124
125        // TODO: Also print whether the index is used elsewhere (for something that is not a
126        // full scan), so that the user knows whether to delete the old index.
127        write!(
128            f,
129            "If your literal equalities filter out many rows, \
130             create an index whose key exactly matches your literal equalities: \
131             ({recommended_key})."
132        )
133    }
134
135    fn fmt_action(
136        &self,
137        f: &mut fmt::Formatter<'_>,
138        humanizer: &dyn ExprHumanizer,
139        redacted: bool,
140    ) -> fmt::Result {
141        let Some(index_on_id_name) = humanizer.humanize_id_unqualified(self.index_on_id) else {
142            return Ok(());
143        };
144
145        let mode = HumanizedNotice::new(redacted);
146        let col_names = humanizer.column_names_for_id(self.index_on_id);
147
148        let recommended_key = mode.seq(&self.recommended_key, col_names.as_ref());
149        let recommended_key = separated(", ", recommended_key);
150
151        write!(f, "CREATE INDEX ON {index_on_id_name}({recommended_key});")
152    }
153
154    fn action_kind(&self, humanizer: &dyn ExprHumanizer) -> ActionKind {
155        match humanizer.humanize_id_unqualified(self.index_on_id) {
156            Some(_) => ActionKind::SqlStatements,
157            None => ActionKind::None,
158        }
159    }
160}