Conversation
Adds sqllogictest and benchmark coverage for correlated `NOT IN`, pinned to what DataFusion does today. Several of these expectations are wrong, and a few shapes do not plan at all; each such block carries a note and a link to apache#25336. The fixes flip them, so the flip is visible in those diffs rather than buried in a large change. sqllogictest, in `null_aware_anti_join.slt` and `null_aware_mark_join.slt`: - correlated `NOT IN` with a non-equality correlation, which stays a residual join filter; - a correlation that names only outer columns, so it cannot become an equi-join key; - a constant value expression with and without a correlation; - a subquery inside the `IN` value, both spellings of the outer `IN`; - `IS NOT NULL` over a subquery predicate and a comparison between two marks, the contexts that can tell a NULL mark from a FALSE mark; - plan pins for the joins the planner chooses in each case. Benchmarks: - Q09, a correlated non-negated `IN`. It must not use a null-aware join, a direction none of Q01-Q08 covers, and it passes today; - correctness canaries on Q05-Q08, each comparing the `NOT IN` result with a reference that does not use `NOT IN`. All four currently disagree, so they are pinned to `false`. Expected results verified with DuckDB and PostgreSQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…NOWN
A correlated `NOT IN` whose correlation is not an equi-join key leaves a
residual join filter. The null-aware hash join ignored it when deciding
whether a NULL on the subquery side makes `NOT IN` UNKNOWN, so a NULL that the
filter excludes still poisoned every outer row:
CREATE TABLE oc(id INT, g INT) AS VALUES (1,5),(2,5),(3,0),(4,NULL),(NULL,5),(NULL,0);
CREATE TABLE ic(id INT) AS VALUES (1),(NULL);
SELECT id, g FROM oc WHERE oc.id NOT IN (SELECT ic.id FROM ic WHERE oc.g > 0);
returned no rows; DuckDB and PostgreSQL return three. The plan was already
correct, `LeftAnti ... Filter: oc.g > Int32(0) null_aware`, so this is purely
an execution fix.
A NULL now makes `NOT IN` UNKNOWN only for the build rows whose correlation
scope and residual filter keep that NULL, recorded per build row in a
null-indices bitmap. Candidates come from a scope-map lookup when there are
correlation keys and from a cross product otherwise, then pass the filter. The
cost is proportional to the number of NULLs and is zero when the data has
none; build rows already marked UNKNOWN are skipped.
Null-aware `LeftAnti` also accepts more than one join key now, which the
equality-correlated shape needs; `RightAnti` still requires exactly one.
Flips the affected expectations from the preceding commit, and the Q05-Q07
correctness canaries.
Closes apache#25336
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decorrelation asked for null-aware semantics only for some `NOT IN` mark
joins. A non-equality correlation leaves a residual join filter rather than an
equi-join key, and that shape was planned without it, so a NULL on the
subquery side read as FALSE instead of UNKNOWN:
SELECT id FROM oc
WHERE oc.h > 1 OR oc.id NOT IN (SELECT i.id FROM ic i WHERE i.k < oc.g);
returned an extra row. The preceding commit gives the executor the machinery;
this asks for it in the remaining place.
A mark join needs it only where a NULL mark can behave differently from a
FALSE mark. `AND`/`OR` give TRUE only from TRUE, and a `Filter` keeps a row
only when the predicate is TRUE, so a non-negated `IN` reached through nothing
but `AND`/`OR` is identical with a FALSE mark and must stay on the plain join:
requesting it there cost 1-2 ms -> 7.4 s at 100k x 100k.
That test is made per subquery occurrence, in a recursion that only knows
`AND`/`OR`. The permissive outcome lives in one arm whose pattern is its own
proof, and every other expression, including future `Expr` variants, takes the
null-aware branch. Deciding it once per `WHERE` conjunct instead let a
sibling's shape change an unrelated subquery's join, and needed a second
enumeration of truth contexts that missed `InSubquery.expr`.
Also: a constant `IN` value is projected as an outer column so the equality
becomes `on[0]`, the key position the executor reads as the `NOT IN` value
key; and `build_join` computes `null_aware` once rather than re-deriving it
for the projection, the mark branch and the anti branch.
Flips the remaining expectations from the first commit, and the Q08 canary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #25560 +/- ##
========================================
Coverage 82.42% 82.42%
========================================
Files 1138 1138
Lines 435429 435588 +159
Branches 435429 435588 +159
========================================
+ Hits 358889 359051 +162
- Misses 54839 54845 +6
+ Partials 21701 21692 -9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Which issue does this PR close?
Important
Stacked on #25558 and #25559. GitHub cannot base a PR on another fork's branch, so the diff here shows all three. Review only the last commit (
fix: request a null-aware mark join wherever a NULL mark is observable). Draft until the other two land; I will rebase then.Rationale for this change
Decorrelation asked for null-aware semantics only for some
NOT INmark joins. A non-equality correlation leaves a residual join filter rather than an equi-join key, and that shape was planned without it, so a NULL on the subquery side read as FALSE instead of UNKNOWN:returns
2, 3, 9; DuckDB returns3, 9. Rowid = 2is UNKNOWN, not TRUE.#25559 gives the executor the machinery. This asks for it in the remaining place.
What changes are included in this PR?
A mark join needs null-aware semantics only where a NULL mark can behave differently from a FALSE mark.
AND/ORgive TRUE only from TRUE, and aFilterkeeps a row only when the predicate is TRUE, so a non-negatedINreached through nothing butAND/ORis identical with a FALSE mark and must stay on the plain join. Requesting it there is expensive: 1–2 ms → 7.4 s at 100k × 100k.That test is made per subquery occurrence, in a recursion that only knows
AND/OR. The permissive outcome lives in one arm whose pattern is its own proof — a non-negatedINwhose value holds no subquery, reached through nothing butAND/ORframes — and everything else, including futureExprvariants, takes the null-aware branch.Two further changes in
build_join:INvalue is projected as an outer column so the equality becomeson[0], the key position the executor reads as theNOT INvalue key. Otherwise a correlation takes that slot and the value-key NULL rules are applied to the wrong key;null_awareis computed once instead of being re-derived for the constant projection, the mark branch and the anti branch.Net effect on the optimizer source is −14 lines.
What is the testing strategy for this PR?
The coverage landed in #25558; this PR flips the remaining expectations, including the Q08 canary, so the diff shows the behaviour change. The plan pins added in #25558 guard the negative direction — that a positive
INstays on the plain join — which no result assertion can catch, since a needless null-aware join is correct, only slower. Mutation testing over the decision confirms it: mutants that widen null-awareness are killed only by those pins.Are there any user-facing changes?
Correlated
NOT INinside a larger predicate returns correct results.🤖 Generated with Claude Code