Skip to content

Commit 06ee435

Browse files
committed
optimizer: avoid deconstructing temporal filters in ANF
1 parent 522c895 commit 06ee435

File tree

3 files changed

+96
-2
lines changed

3 files changed

+96
-2
lines changed

src/expr/src/linear.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,7 @@ pub fn memoize_expr(
14901490
if let MirScalarExpr::If { cond, .. } = e {
14911491
return Some(vec![cond]);
14921492
}
1493+
14931494
// We should not eagerly memoize `COALESCE` expressions after the first,
14941495
// as they are only meant to be evaluated if the preceding expressions
14951496
// evaluate to NULL. We could memoize any preceding by expressions that
@@ -1501,6 +1502,15 @@ pub fn memoize_expr(
15011502
{
15021503
return Some(exprs.iter_mut().take(1).collect());
15031504
}
1505+
1506+
// We should not deconstruct temporal filters, because `MfpPlan::create_from` expects
1507+
// those to be in a specific form. However, we _should_ attend to the expression that is
1508+
// on the opposite side of mz_now(), because it might be a complex expression in itself,
1509+
// and is ok to deconstruct.
1510+
if let Some(other_side) = e.is_temporal_filter() {
1511+
return Some(vec![other_side]);
1512+
}
1513+
15041514
None
15051515
},
15061516
&mut |e| {
@@ -1520,6 +1530,8 @@ pub fn memoize_expr(
15201530
}
15211531
}
15221532
_ => {
1533+
// TODO: OOO (Optimizer Optimization Opportunity):
1534+
// we are quadratic in expression size because of this .iter().position
15231535
if let Some(position) = memoized_parts.iter().position(|e2| e2 == e) {
15241536
// Any complex expression that already exists as a prior column can
15251537
// be replaced by a reference to that column.
@@ -1634,6 +1646,7 @@ pub mod util {
16341646
pub mod plan {
16351647
use std::iter;
16361648

1649+
use mz_ore::soft_assert_or_log;
16371650
use mz_proto::{IntoRustIfSome, ProtoType, RustType, TryFromProtoError};
16381651
use mz_repr::{Datum, Diff, Row, RowArena};
16391652
use proptest::prelude::*;
@@ -1842,7 +1855,8 @@ pub mod plan {
18421855
}
18431856
});
18441857

1845-
for predicate in temporal.into_iter() {
1858+
for mut predicate in temporal.into_iter() {
1859+
let is_temporal_filter = predicate.is_temporal_filter().is_some(); // for the asserts
18461860
// Supported temporal predicates are exclusively binary operators.
18471861
if let MirScalarExpr::CallBinary {
18481862
mut func,
@@ -1863,6 +1877,10 @@ pub mod plan {
18631877
BinaryFunc::Gt => BinaryFunc::Lt,
18641878
BinaryFunc::Gte => BinaryFunc::Lte,
18651879
x => {
1880+
soft_assert_or_log!(
1881+
!is_temporal_filter,
1882+
"MfpPlan::create_from and is_temporal_filter should agree"
1883+
);
18661884
return Err(format!(
18671885
"Unsupported binary temporal operation: {:?}",
18681886
x
@@ -1876,6 +1894,10 @@ pub mod plan {
18761894
|| *expr1
18771895
!= MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow)
18781896
{
1897+
soft_assert_or_log!(
1898+
!is_temporal_filter,
1899+
"MfpPlan::create_from and is_temporal_filter should agree"
1900+
);
18791901
return Err(format!(
18801902
"Unsupported temporal predicate. Note: `mz_now()` must be directly compared to a mz_timestamp-castable expression. Expression found: {}",
18811903
MirScalarExpr::CallBinary { func, expr1, expr2 },
@@ -1913,7 +1935,15 @@ pub mod plan {
19131935
));
19141936
}
19151937
}
1938+
soft_assert_or_log!(
1939+
is_temporal_filter, // successfully found a temporal filter
1940+
"MfpPlan::create_from and is_temporal_filter should agree"
1941+
);
19161942
} else {
1943+
soft_assert_or_log!(
1944+
!is_temporal_filter,
1945+
"MfpPlan::create_from and is_temporal_filter should agree"
1946+
);
19171947
return Err(format!(
19181948
"Unsupported temporal predicate. Note: `mz_now()` must be directly compared to a non-temporal expression of mz_timestamp-castable type. Expression found: {}",
19191949
predicate,

src/expr/src/scalar.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,27 @@ impl MirScalarExpr {
728728
)
729729
}
730730

731+
/// Checks whether the expression is a standard temporal filter, that is:
732+
/// - mz_now() by itself on one side of an Eq | Gt | Gte | Lt | Lte comparison, and
733+
/// - no mz_now() anywhere inside the other side of the comparison.
734+
///
735+
/// If yes, then it returns the other side of the comparison.
736+
pub fn is_temporal_filter(&mut self) -> Option<&mut MirScalarExpr> {
737+
use BinaryFunc::*;
738+
if let MirScalarExpr::CallBinary { func, expr1, expr2 } = self {
739+
if matches!(func, Eq | Gt | Gte | Lt | Lte) {
740+
let mz_now = MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow);
741+
if **expr1 == mz_now && !expr2.contains_temporal() {
742+
return Some(expr2);
743+
}
744+
if **expr2 == mz_now && !expr1.contains_temporal() {
745+
return Some(expr1);
746+
}
747+
}
748+
}
749+
None
750+
}
751+
731752
#[deprecated = "Use `might_error` instead"]
732753
pub fn contains_error_if_null(&self) -> bool {
733754
let mut worklist = vec![self];

test/sqllogictest/temporal.slt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ WHERE insert_ts >= mz_now() - 30;
245245
statement ok
246246
CREATE TABLE t2(ts timestamp, x int);
247247

248+
statement ok
249+
INSERT INTO t2 VALUES ('2024-01-03 00:00:00', 5), ('2024-01-04 00:00:00', 6);
250+
248251
statement ok
249252
SELECT *
250253
FROM t2
@@ -256,6 +259,9 @@ SELECT *
256259
FROM t2
257260
WHERE ts + INTERVAL '30' minutes >= mz_now();
258261

262+
statement ok
263+
SELECT * FROM mv1;
264+
259265
query error db error: ERROR: WHERE clause error: operator does not exist: mz_timestamp \- interval
260266
SELECT *
261267
FROM t2
@@ -313,15 +319,21 @@ WHERE
313319
ts + INTERVAL '30' minutes >= mz_now();
314320

315321
statement ok
316-
SELECT * FROM v_one_off;
322+
SELECT * FROM v_maintained;
317323

318324
statement ok
319325
CREATE DEFAULT INDEX ON v_maintained;
320326

327+
statement ok
328+
SELECT * FROM v_maintained;
329+
321330
statement ok
322331
CREATE MATERIALIZED VIEW mv2 AS
323332
SELECT * FROM v_maintained;
324333

334+
statement ok
335+
SELECT * FROM mv2;
336+
325337
simple
326338
DECLARE c CURSOR FOR SUBSCRIBE v_maintained;
327339
FETCH 0 c;
@@ -338,6 +350,9 @@ WHERE
338350
ts + INTERVAL '30' minutes >= mz_now()
339351
AND x != 7;
340352

353+
statement ok
354+
SELECT * FROM mv3;
355+
341356
# UNION ALL workaround for OR
342357

343358
query error db error: ERROR: Unsupported temporal predicate\. Note: `mz_now\(\)` must be directly compared to a non\-temporal expression of mz_timestamp\-castable type\. Expression found: \(\(#1\{x\} = 7\) OR \(timestamp_to_mz_timestamp\(\(#0\{ts\} \+ 00:30:00\)\) >= mz_now\(\)\)\)
@@ -363,3 +378,31 @@ UNION ALL
363378
ts + INTERVAL '30' minutes >= mz_now()
364379
AND x != 7
365380
);
381+
382+
statement ok
383+
SELECT * FROM mv4;
384+
385+
# Regression test for https://github.com/MaterializeInc/database-issues/issues/9643
386+
# Two different temporal filters on different reads of the same source.
387+
388+
statement ok
389+
SELECT *
390+
FROM t2
391+
WHERE ts + INTERVAL '30' minutes >= mz_now()
392+
UNION ALL
393+
SELECT *
394+
FROM t2
395+
WHERE ts + INTERVAL '60' minutes >= mz_now();
396+
397+
statement ok
398+
CREATE MATERIALIZED VIEW mv5 AS
399+
SELECT *
400+
FROM t2
401+
WHERE ts + INTERVAL '30' minutes >= mz_now()
402+
UNION ALL
403+
SELECT *
404+
FROM t2
405+
WHERE ts + INTERVAL '60' minutes >= mz_now();
406+
407+
statement ok
408+
SELECT * FROM mv5;

0 commit comments

Comments
 (0)