sql: collapse FROM-less correlated existence subqueries to a filter#37442
Open
antiguru wants to merge 2 commits into
Open
sql: collapse FROM-less correlated existence subqueries to a filter#37442antiguru wants to merge 2 commits into
antiguru wants to merge 2 commits into
Conversation
0ef6d98 to
75f4564
Compare
A FROM-less correlated existence subquery is a pure predicate on the outer row, but decorrelation lowers it into a semijoin or antijoin that the MIR transforms do not collapse back to a filter. This leaves avoidable joins in the plan for the shapes reported in database-issues#2613 (`1 IN (SELECT 1 WHERE p)`) and database-issues#2969 (`NOT EXISTS (SELECT 1 WHERE p)`). Add an HIR simplification pass, `simplify_from_less_existence_subqueries`, that runs after `try_simplify_quantified_comparisons` (which already normalizes both `IN` and `EXISTS` shapes to an `Exists` node). When the `Exists` body is a FROM-less correlated chain of `Map`/`Project`/`Filter` over a single-row constant, it rewrites `EXISTS(chain, preds)` to `(preds) IS TRUE`, inlining inner column references and shifting outer ones down one level as the predicate leaves the subquery. The `IS TRUE` wrapper is load-bearing for null safety. `NOT EXISTS` keeps the outer row when the subquery is empty, which is when the predicate is FALSE or NULL. `NOT ((p) IS TRUE)` = `p IS NOT TRUE` is true for both, matching. A plain `NOT p` would drop the NULL row. The pass is gated behind the `enable_simplify_from_less_existence` feature flag, default off in production and on in CI and tests. The guard fires only on the FROM-less correlated pure-existence shape, so subqueries with a FROM clause remain genuine anti/semi-joins. Tests: subquery.slt covers both issues with EXPLAIN before/after in the flag-off and flag-on states, plus an explicit NULL-row test for the antijoin rewrite. not-null-propagation.slt is unchanged with the flag defaulting off. Fixes database-issues#2613 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75f4564 to
6df045a
Compare
def-
requested changes
Jul 6, 2026
def-
left a comment
Contributor
There was a problem hiding this comment.
Extra test:
diff --git a/test/sqllogictest/subquery.slt b/test/sqllogictest/subquery.slt
index 4ab64b8cf1..35a57451ff 100644
--- a/test/sqllogictest/subquery.slt
+++ b/test/sqllogictest/subquery.slt
@@ -895,6 +895,45 @@ Target cluster: quickstart
EOF
+# A window function in a FROM-less quantified subquery must not be inlined into
+# the outer Filter by the collapse. `x IN/= ANY/= ALL (SELECT <expr>)` normalizes
+# to an EXISTS with `<expr>` in a Filter predicate, but a window function run over
+# the whole outer relation (row_number() = 1, 2, 3, ...) differs from its
+# single-row subquery value (always 1). All three `1` rows satisfy `f1 = 1`, so
+# the correct answer keeps all three. The buggy inlined form keeps only the one
+# row that row_number() numbers 1.
+statement ok
+CREATE TABLE flw (f1 int)
+
+statement ok
+INSERT INTO flw VALUES (1), (1), (1), (NULL)
+
+simple conn=mz_system,user=mz_system
+ALTER SYSTEM SET enable_simplify_from_less_existence TO true;
+----
+COMPLETE 0
+
+query I rowsort
+SELECT f1 FROM flw WHERE f1 IN (SELECT row_number() OVER ())
+----
+1
+1
+1
+
+query I rowsort
+SELECT f1 FROM flw WHERE f1 = ANY (SELECT row_number() OVER ())
+----
+1
+1
+1
+
+query I rowsort
+SELECT f1 FROM flw WHERE f1 = ALL (SELECT row_number() OVER ())
+----
+1
+1
+1
+
# Restore the default so later files see production behavior.
simple conn=mz_system,user=mz_system
ALTER SYSTEM SET enable_simplify_from_less_existence TO false;Fails with this PR, worked fine before:
--- test/sqllogictest/subquery.slt
SELECT f1 FROM flw WHERE f1 IN (SELECT row_number() OVER ())
OutputFailure:test/sqllogictest/subquery.slt:917
expected: Values(["1", "1", "1"])
actually: Values(["1"])
actual raw: [Row { columns: [Column { name: "f1", table_oid: None, column_id: None, type: Int4 }] }]
----
SELECT f1 FROM flw WHERE f1 = ANY (SELECT row_number() OVER ())
OutputFailure:test/sqllogictest/subquery.slt:924
expected: Values(["1", "1", "1"])
actually: Values(["1"])
actual raw: [Row { columns: [Column { name: "f1", table_oid: None, column_id: None, type: Int4 }] }]
----
SELECT f1 FROM flw WHERE f1 = ALL (SELECT row_number() OVER ())
OutputFailure:test/sqllogictest/subquery.slt:931
expected: Values(["1", "1", "1"])
actually: Values(["1"])
actual raw: [Row { columns: [Column { name: "f1", table_oid: None, column_id: None, type: Int4 }] }]
----
FAIL: output-failure=3 success=93 total=96
Correctness issue, so would be good to fix before merging.
`resolve_local_columns` inlined a subquery's scalars into the outer Filter, but a window function's value depends on the relation it runs over, not just the row. `row_number() OVER ()` over the single-row FROM-less body is always 1, but over the multi-row outer relation it counts 1, 2, 3, ..., so inlining changed results: `f1 IN (SELECT row_number() OVER ())` dropped rows it must keep. Reject window functions alongside subqueries when deciding whether a scalar can be lifted. The classification is an exhaustive match on `HirScalarExpr` so a new variant must be classified rather than silently treated as liftable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Member
Author
|
Great catch. I fixed it by exhaustively matching on the HIR variant, and only allowing the optimization on non-nested expressions. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A FROM-less correlated existence subquery is a pure predicate on the outer row.
Decorrelation lowers it into a semijoin or antijoin that the MIR transforms do not collapse back to a filter.
This leaves avoidable joins in the plan for the shapes reported in database-issues#2613 (
1 IN (SELECT 1 WHERE p)) and database-issues#2969 (NOT EXISTS (SELECT 1 WHERE p)).Add an HIR simplification pass,
simplify_from_less_existence_subqueries, that runs aftertry_simplify_quantified_comparisons(which already normalizes bothINandEXISTSshapes to anExistsnode).When the
Existsbody is a FROM-less correlated chain ofMap/Project/Filterover a single-row constant, it rewritesEXISTS(chain, preds)to(preds) IS TRUE, inlining inner column references and shifting outer ones down one level as the predicate leaves the subquery.The
IS TRUEwrapper is load-bearing for null safety.NOT EXISTSkeeps the outer row when the subquery is empty, which is when the predicate is FALSE or NULL.NOT ((p) IS TRUE)=p IS NOT TRUEis true for both, matching.A plain
NOT pwould drop the NULL row.The guard fires only on the FROM-less correlated pure-existence shape, so subqueries with a FROM clause remain genuine anti/semi-joins.
The pass is gated behind the
enable_simplify_from_less_existencefeature flag, default off in production and on in CI and tests.Tests:
subquery.sltcovers both issues with EXPLAIN before/after in the flag-off and flag-on states, plus an explicit NULL-row test for the antijoin rewrite.Fixes database-issues#2613
Motivation
Fixes a plan-quality gap: FROM-less correlated existence subqueries leave avoidable semi/antijoins.