Skip to content

fix: skip dynamic filter pushdown for null-aware anti joins with a nullable build key - #23173

Merged
alamb merged 3 commits into
apache:mainfrom
paradedb:moe/null-aware-build-null
Jul 26, 2026
Merged

fix: skip dynamic filter pushdown for null-aware anti joins with a nullable build key#23173
alamb merged 3 commits into
apache:mainfrom
paradedb:moe/null-aware-build-null

Conversation

@mdashti

@mdashti mdashti commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Which issue does this close?

Rationale for this change

x NOT IN (subquery) plans to a null-aware LeftAnti hash join (build = outer x, probe = subquery). Join dynamic filter pushdown pushes a bounds + membership filter, built from the build keys, onto the probe scan. That filter can prune every probe row. A null-aware LeftAnti reads an empty probe as a genuinely-empty subquery, so it emits build-side NULL rows that should drop: NULL NOT IN (non-empty) is UNKNOWN, not TRUE.

The result is scan-dependent, so it's a silent correctness bug. A VALUES scan ignores the pushed filter and stays correct; a parquet scan applies it and is wrong.

#23103 (the probe-side NULL drop) is orthogonal; this is the build-side NULL.

What changes are included in this PR?

Skip join dynamic filter pushdown for a null-aware anti join when the build key can be NULL. The build-side NULL emission depends on whether the probe is truly empty, which the pushed filter can change by emptying it. A NOT NULL build key has no such NULL, so it keeps the pushdown.

The check is static: a schema-nullable build key disables the pushdown even when the data contains no NULLs. A runtime alternative (keep the pushdown and neutralize the filter only when the build actually holds a NULL key) would restore the optimization for those cases. I'd leave that as a follow-up.

Are these changes tested?

Yes. A push_down_filter_parquet.slt case reproduces it (build-side NULL, a non-matching parquet probe) and asserts the single correct row. Without the change it returns the extra NULL. In addition, unit tests pin both directions of the guard: a nullable build key rejects the pushdown and a NOT NULL build key keeps it.

Are there any user-facing changes?

NOT IN over a parquet (or otherwise prunable) scan with a nullable outer key now returns correct results. Such joins lose the dynamic filter pushdown.

@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) physical-plan Changes to the physical-plan crate labels Jun 25, 2026
@kosiew

kosiew commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@mdashti
Thanks for working on this.
Can you investigate and resolve the CI test errors?

@neilconway
neilconway self-requested a review July 11, 2026 13:05
@neilconway

Copy link
Copy Markdown
Contributor

@mdashti Can you resolve the CI failures when you get a chance?

mdashti added 3 commits July 20, 2026 10:08
The pushed filter can empty the probe. A null-aware `LeftAnti` then reads it
as an empty subquery and emits build-side NULLs that should be dropped. A
NOT NULL build key is unaffected.
Indexing on[0] leaned on the single-key invariant without saying so.
Iterating all keys survives a future multi-key relaxation. In addition,
added allow/reject unit tests to pin both directions of the guard.
The guard removes the probe-side dynamic filter from these plans.
@mdashti
mdashti force-pushed the moe/null-aware-build-null branch from 32ead3e to e512368 Compare July 20, 2026 17:23
@mdashti

mdashti commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@kosiew @neilconway the failures were stale EXPLAIN expectations in dynamic_filter_pushdown_config.slt and explain_tree.slt that still showed the now-skipped dynamic filter on the probe scan. Rebased on main and updated them in e512368. Also added unit tests for both directions of the guard in 91a7e95.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.65517% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.70%. Comparing base (4184b07) to head (e512368).

Files with missing lines Patch % Lines
...tafusion/physical-plan/src/joins/hash_join/exec.rs 89.65% 2 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #23173      +/-   ##
==========================================
- Coverage   80.70%   80.70%   -0.01%     
==========================================
  Files        1089     1089              
  Lines      368038   368096      +58     
  Branches   368038   368096      +58     
==========================================
+ Hits       297031   297067      +36     
- Misses      53308    53325      +17     
- Partials    17699    17704       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kosiew

kosiew commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

run benchmark tpch

@apache apache deleted a comment from adriangbot Jul 23, 2026
@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5055604641-1244-9bjwr 6.12.85+ #1 SMP Mon May 11 08:17:35 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing moe/null-aware-build-null (e512368) to 4184b07 (merge-base) diff using: tpch
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

Comparing HEAD and moe_null-aware-build-null
--------------------
Benchmark tpch_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query     ┃                           HEAD ┃      moe_null-aware-build-null ┃    Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ QQuery 1  │ 37.98 / 39.08 ±1.16 / 41.09 ms │ 38.04 / 38.70 ±0.85 / 40.38 ms │ no change │
│ QQuery 2  │ 18.87 / 19.77 ±1.19 / 22.09 ms │ 18.83 / 19.25 ±0.30 / 19.70 ms │ no change │
│ QQuery 3  │ 30.88 / 33.31 ±1.62 / 35.95 ms │ 30.41 / 32.38 ±1.49 / 34.08 ms │ no change │
│ QQuery 4  │ 17.27 / 17.72 ±0.48 / 18.64 ms │ 17.38 / 17.94 ±0.66 / 19.17 ms │ no change │
│ QQuery 5  │ 37.42 / 40.62 ±1.93 / 43.14 ms │ 37.69 / 40.55 ±1.71 / 43.07 ms │ no change │
│ QQuery 6  │ 16.15 / 16.27 ±0.11 / 16.44 ms │ 16.19 / 16.86 ±0.64 / 17.92 ms │ no change │
│ QQuery 7  │ 42.16 / 43.71 ±1.12 / 45.36 ms │ 43.77 / 45.31 ±1.83 / 48.89 ms │ no change │
│ QQuery 8  │ 42.57 / 43.93 ±1.36 / 46.47 ms │ 42.62 / 43.22 ±0.78 / 44.74 ms │ no change │
│ QQuery 9  │ 48.65 / 49.69 ±1.18 / 51.94 ms │ 48.49 / 50.62 ±1.35 / 52.27 ms │ no change │
│ QQuery 10 │ 41.67 / 42.57 ±1.27 / 45.04 ms │ 42.11 / 42.27 ±0.11 / 42.42 ms │ no change │
│ QQuery 11 │ 13.21 / 13.54 ±0.40 / 14.30 ms │ 13.28 / 13.47 ±0.21 / 13.85 ms │ no change │
│ QQuery 12 │ 23.51 / 23.67 ±0.14 / 23.90 ms │ 23.26 / 24.10 ±0.53 / 24.88 ms │ no change │
│ QQuery 13 │ 32.83 / 34.03 ±1.37 / 36.50 ms │ 31.50 / 32.82 ±1.03 / 34.33 ms │ no change │
│ QQuery 14 │ 23.35 / 23.71 ±0.35 / 24.29 ms │ 23.33 / 23.61 ±0.24 / 23.91 ms │ no change │
│ QQuery 15 │ 31.19 / 32.06 ±0.60 / 32.90 ms │ 30.81 / 31.99 ±0.94 / 33.33 ms │ no change │
│ QQuery 16 │ 13.97 / 14.29 ±0.31 / 14.77 ms │ 13.82 / 14.04 ±0.20 / 14.35 ms │ no change │
│ QQuery 17 │ 69.97 / 70.80 ±1.01 / 72.75 ms │ 69.89 / 70.93 ±1.21 / 73.31 ms │ no change │
│ QQuery 18 │ 59.13 / 60.84 ±1.26 / 62.12 ms │ 57.98 / 60.48 ±1.79 / 62.96 ms │ no change │
│ QQuery 19 │ 32.78 / 33.29 ±0.68 / 34.56 ms │ 33.10 / 33.57 ±0.34 / 34.07 ms │ no change │
│ QQuery 20 │ 31.36 / 31.62 ±0.22 / 31.93 ms │ 31.56 / 32.13 ±0.47 / 32.81 ms │ no change │
│ QQuery 21 │ 54.10 / 54.83 ±0.80 / 56.16 ms │ 53.41 / 55.57 ±1.34 / 57.59 ms │ no change │
│ QQuery 22 │ 13.71 / 13.86 ±0.11 / 14.00 ms │ 13.78 / 13.94 ±0.18 / 14.29 ms │ no change │
└───────────┴────────────────────────────────┴────────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Benchmark Summary                        ┃          ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Total Time (HEAD)                        │ 753.23ms │
│ Total Time (moe_null-aware-build-null)   │ 753.72ms │
│ Average Time (HEAD)                      │  34.24ms │
│ Average Time (moe_null-aware-build-null) │  34.26ms │
│ Queries Faster                           │        0 │
│ Queries Slower                           │        0 │
│ Queries with No Change                   │       22 │
│ Queries with Failure                     │        0 │
└──────────────────────────────────────────┴──────────┘

Resource Usage

tpch — base (merge-base)

Metric Value
Wall time 5.0s
Peak memory 1.2 GiB
Avg memory 513.8 MiB
CPU user 21.5s
CPU sys 1.6s
Peak spill 0 B

tpch — branch

Metric Value
Wall time 5.0s
Peak memory 1.2 GiB
Avg memory 527.4 MiB
CPU user 21.5s
CPU sys 1.7s
Peak spill 0 B

File an issue against this benchmark runner

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdashti

Looks 👍 to me

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kosiew and @mdashti

return false;
}

// A null-aware anti join emits a build-side NULL only when the probe

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment says anto-join but the check isn't for join type. I wonder if that is a discrepancy?

@alamb
alamb added this pull request to the merge queue Jul 26, 2026
Merged via the queue into apache:main with commit 5975a2d Jul 26, 2026
43 checks passed
@philippemnoel
philippemnoel deleted the moe/null-aware-build-null branch July 28, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-plan Changes to the physical-plan crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NOT IN + dynamic filters + build-side NULL returns incorrect results

6 participants