Skip to content

Commit 7ee105b

Browse files
committed
Add an EXPLAIN option to show the Equivalences Analysis
1 parent 9270d55 commit 7ee105b

File tree

8 files changed

+90
-1
lines changed

8 files changed

+90
-1
lines changed

src/repr/src/explain.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ pub struct ExplainConfig {
170170
pub cardinality: bool,
171171
/// Show the `ColumnNames` Analysis.
172172
pub column_names: bool,
173+
/// Show the `Equivalences` Analysis.
174+
pub equivalences: bool,
175+
// TODO: add an option to show the `Monotonic` Analysis. This is non-trivial, because this
176+
// Analysis needs the set of monotonic GlobalIds, which are cumbersome to pass around.
173177

174178
// Other display options:
175179
/// Render implemented MIR `Join` nodes in a way which reflects the implementation.
@@ -222,6 +226,7 @@ impl Default for ExplainConfig {
222226
subtree_size: false,
223227
timing: false,
224228
types: false,
229+
equivalences: false,
225230
features: Default::default(),
226231
}
227232
}
@@ -236,6 +241,7 @@ impl ExplainConfig {
236241
|| self.keys
237242
|| self.cardinality
238243
|| self.column_names
244+
|| self.equivalences
239245
}
240246
}
241247

@@ -629,6 +635,7 @@ pub struct Analyses {
629635
pub keys: Option<Vec<Vec<usize>>>,
630636
pub cardinality: Option<String>,
631637
pub column_names: Option<Vec<String>>,
638+
pub equivalences: Option<String>,
632639
}
633640

634641
#[derive(Debug, Clone)]
@@ -716,6 +723,11 @@ impl<'a> Display for HumanizedAnalyses<'a> {
716723
builder.field("column_names", &column_names);
717724
}
718725

726+
if self.config.equivalences {
727+
let equivs = self.analyses.equivalences.as_ref().expect("equivalences");
728+
builder.field("equivs", equivs);
729+
}
730+
719731
builder.finish()
720732
}
721733
}
@@ -938,6 +950,7 @@ mod tests {
938950
raw_plans: false,
939951
raw_syntax: false,
940952
subtree_size: false,
953+
equivalences: false,
941954
timing: true,
942955
types: false,
943956
features: Default::default(),

src/sql-lexer/src/keywords.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ End
151151
Endpoint
152152
Enforced
153153
Envelope
154+
Equivalences
154155
Error
155156
Errors
156157
Escape

src/sql-parser/src/ast/defs/statement.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3883,6 +3883,7 @@ pub enum ExplainPlanOptionName {
38833883
SubtreeSize,
38843884
Timing,
38853885
Types,
3886+
Equivalences,
38863887
ReoptimizeImportedViews,
38873888
EnableNewOuterJoinLowering,
38883889
EnableEagerDeltaJoins,
@@ -3917,6 +3918,7 @@ impl WithOptionName for ExplainPlanOptionName {
39173918
| Self::SubtreeSize
39183919
| Self::Timing
39193920
| Self::Types
3921+
| Self::Equivalences
39203922
| Self::ReoptimizeImportedViews
39213923
| Self::EnableNewOuterJoinLowering
39223924
| Self::EnableEagerDeltaJoins

src/sql/src/plan/statement/dml.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ generate_extracted_config!(
364364
(SubtreeSize, bool, Default(false)),
365365
(Timing, bool, Default(false)),
366366
(Types, bool, Default(false)),
367+
(Equivalences, bool, Default(false)),
367368
(ReoptimizeImportedViews, Option<bool>, Default(None)),
368369
(EnableNewOuterJoinLowering, Option<bool>, Default(None)),
369370
(EnableEagerDeltaJoins, Option<bool>, Default(None)),
@@ -406,6 +407,7 @@ impl TryFrom<ExplainPlanOptionExtracted> for ExplainConfig {
406407
raw_syntax: v.raw_syntax,
407408
redacted: v.redacted,
408409
subtree_size: v.subtree_size,
410+
equivalences: v.equivalences,
409411
timing: v.timing,
410412
types: v.types,
411413
// The ones that are initialized with `Default::default()` are not wired up to EXPLAIN.

src/transform/src/analysis.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,8 @@ mod explain {
12051205
use mz_ore::stack::RecursionLimitError;
12061206
use mz_repr::explain::{Analyses, AnnotatedPlan};
12071207

1208+
use crate::analysis::equivalences::Equivalences;
1209+
12081210
// Analyses should have shortened paths when exported.
12091211
use super::DerivedBuilder;
12101212

@@ -1234,6 +1236,9 @@ mod explain {
12341236
if context.config.column_names || context.config.humanized_exprs {
12351237
builder.require(super::ColumnNames);
12361238
}
1239+
if context.config.equivalences {
1240+
builder.require(Equivalences);
1241+
}
12371242
builder
12381243
}
12391244
}
@@ -1333,6 +1338,19 @@ mod explain {
13331338
analyses.column_names = Some(value);
13341339
}
13351340
}
1341+
1342+
if config.equivalences {
1343+
for (expr, equivs) in std::iter::zip(
1344+
subtree_refs.iter(),
1345+
derived.results::<Equivalences>().unwrap().into_iter(),
1346+
) {
1347+
let analyses = annotations.entry(expr).or_default();
1348+
analyses.equivalences = Some(match equivs.as_ref() {
1349+
Some(equivs) => equivs.to_string(),
1350+
None => "<empty collection>".to_string(),
1351+
});
1352+
}
1353+
}
13361354
}
13371355

13381356
Ok(AnnotatedPlan { plan, annotations })

src/transform/src/analysis/equivalences.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
//! equivalences classes, each a list of equivalent expressions.
1717
1818
use std::collections::BTreeMap;
19+
use std::fmt::Formatter;
1920

2021
use mz_expr::{Id, MirRelationExpr, MirScalarExpr};
22+
use mz_ore::str::{bracketed, separated};
2123
use mz_repr::{ColumnType, Datum};
2224

2325
use crate::analysis::{Analysis, Lattice};
@@ -318,7 +320,7 @@ pub struct EquivalenceClasses {
318320
/// The first element should be the "canonical" simplest element, that any other element
319321
/// can be replaced by.
320322
/// These classes are unified whenever possible, to minimize the number of classes.
321-
/// They are only guaranteed to form an equivalence relation after a call to `minimimize`,
323+
/// They are only guaranteed to form an equivalence relation after a call to `minimize`,
322324
/// which refreshes both `self.classes` and `self.remap`.
323325
pub classes: Vec<Vec<MirScalarExpr>>,
324326

@@ -334,6 +336,17 @@ pub struct EquivalenceClasses {
334336
remap: BTreeMap<MirScalarExpr, MirScalarExpr>,
335337
}
336338

339+
impl std::fmt::Display for EquivalenceClasses {
340+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
341+
// Only show `classes`.
342+
let classes = self
343+
.classes
344+
.iter()
345+
.map(|class| format!("{}", bracketed("[", "]", separated(", ", class))));
346+
write!(f, "{}", bracketed("[", "]", separated(", ", classes)))
347+
}
348+
}
349+
337350
impl EquivalenceClasses {
338351
/// Comparator function for the complexity of scalar expressions. Simpler expressions are
339352
/// smaller. Can be used when we need to decide which of several equivalent expressions to use.

src/transform/tests/test_runner.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ mod tests {
141141
raw_plans: false,
142142
raw_syntax: false,
143143
subtree_size: false,
144+
equivalences: false,
144145
timing: false,
145146
types: format_contains("types"),
146147
..ExplainConfig::default()

test/sqllogictest/explain/optimized_plan_as_text.slt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,3 +1577,42 @@ Source materialize.public.t4
15771577
Target cluster: no_replicas
15781578

15791579
EOF
1580+
1581+
statement ok
1582+
CREATE TABLE t5(
1583+
x int,
1584+
y int NOT NULL,
1585+
z int
1586+
);
1587+
1588+
statement ok
1589+
CREATE TABLE t6(
1590+
a int NOT NULL,
1591+
b int
1592+
);
1593+
1594+
# WITH(EQUIVALENCES)
1595+
query T multiline
1596+
EXPLAIN WITH(EQUIVALENCES)
1597+
SELECT *
1598+
FROM t5, t6
1599+
WHERE x = a AND b IN (8,9);
1600+
----
1601+
Explained Query:
1602+
Project (#0..=#2, #0, #4) // { equivs: "[[#0, #3], [false, (#0) IS NULL, (#1) IS NULL], [true, ((#4 = 8) OR (#4 = 9))]]" }
1603+
Join on=(#0 = #3) type=differential // { equivs: "[[#0, #3], [false, (#0) IS NULL, (#1) IS NULL], [true, ((#4 = 8) OR (#4 = 9))]]" }
1604+
ArrangeBy keys=[[#0]] // { equivs: "[[false, (#0) IS NULL, (#1) IS NULL]]" }
1605+
Filter (#0) IS NOT NULL // { equivs: "[[false, (#0) IS NULL, (#1) IS NULL]]" }
1606+
ReadStorage materialize.public.t5 // { equivs: "[[false, (#1) IS NULL]]" }
1607+
ArrangeBy keys=[[#0]] // { equivs: "[[false, (#0) IS NULL], [true, ((#1 = 8) OR (#1 = 9))]]" }
1608+
Filter ((#1 = 8) OR (#1 = 9)) // { equivs: "[[false, (#0) IS NULL], [true, ((#1 = 8) OR (#1 = 9))]]" }
1609+
ReadStorage materialize.public.t6 // { equivs: "[[false, (#0) IS NULL]]" }
1610+
1611+
Source materialize.public.t5
1612+
filter=((#0) IS NOT NULL)
1613+
Source materialize.public.t6
1614+
filter=(((#1 = 8) OR (#1 = 9)))
1615+
1616+
Target cluster: no_replicas
1617+
1618+
EOF

0 commit comments

Comments
 (0)