Skip to content

feat: support multi-column NOT IN subqueries with null-aware joins - #19857

Merged
viirya merged 7 commits into
apache:mainfrom
viirya:multi-column-null-aware-anti-join
Oct 7, 2026
Merged

viirya merged 7 commits into
apache:mainfrom
viirya:multi-column-null-aware-anti-join

Conversation

@viirya

@viirya viirya commented Jan 16, 2026 •

Copy link
Copy Markdown
Member

Which issue does this PR close?

Follow-up to #10583, which made single-column NOT IN a null-aware anti join.

Rationale for this change

Multi-column IN / NOT IN subqueries fail to plan:

SELECT * FROM t1 WHERE (a, b) NOT IN (SELECT x, y FROM t2);

SELECT * FROM t1 WHERE (c2, c3) NOT IN (
  SELECT c2, c3 FROM t2 WHERE t1.c1 = t2.c1
);

Supporting them requires SQL three-valued logic over the whole tuple. A NULL in one element does not make every comparison UNKNOWN the way a scalar NULL does: (NULL, 8) = (1, 2) is UNKNOWN AND FALSE, which is FALSE, so the outer row is kept. (NULL, 2) = (1, 2) is UNKNOWN, so it is dropped.

What changes are included in this PR?

A null-aware join already reads its equi-join keys by position: on[0] is the NOT IN value key and on[1..] are the correlation scope keys of a correlated subquery. A tuple needs several value keys, so both the logical Join and HashJoinExec now record how many leading keys are value keys (null_aware_value_keys). The layout becomes on[..V] for the tuple elements and on[V..] for the correlation keys.

  • SQL planning / analyzer: (a, b) IN (SELECT x, y ...) is accepted when the tuple and subquery column counts match. Type coercion and placeholder inference work element by element. A tuple compared with a single struct-typed column ((a, b) IN (SELECT s ...)) is still a scalar struct comparison.
  • Decorrelation: emits one equality per tuple element, ahead of the correlation predicates, so the elements become the leading keys in order. Constant elements are projected as outer columns, the same way the scalar constant case already is. A tuple IN / NOT IN inside a projection or an OR goes through a null-aware mark join.
  • HashJoinExec: V > 1 uses the existing per-build-row path of correlated null-aware joins. A row counts as NULL-valued when any value key is NULL. Candidate pairs are then kept only when their value tuples have no definite mismatch, i.e. every element pair is equal or involves a NULL. This works for both LeftAnti and LeftMark, with or without scope keys and join filters.
  • Dynamic filter pushdown: probe rows with a NULL in any value key are kept.
  • Protobuf: JoinNode and HashJoinExecNode carry null_aware_value_keys. A missing field (0) decodes as 1.

What is the testing strategy for this PR?

  • null_aware_anti_join.slt covers:
    • 2- and 3-column NOT IN with NULLs on either side, an empty subquery, and correlated equality and non-equality correlations
    • constant tuple elements
    • NOT IN as a nullable projected value and under OR
    • per-element type coercion, column count errors, a single struct column, and placeholders
  • subquery.slt covers multi-column IN.
  • hash_join/exec.rs unit tests run LeftAnti and LeftMark with probe-side and build-side NULLs, three columns, an empty probe side, and correlated scope keys, across batch sizes. Further unit tests cover value-key validation and the protobuf round trip. shared_bounds.rs tests the dynamic filter NULL escape over every value key.
  • roundtrip_logical_plan.rs round-trips a correlated multi-column NOT IN plan.

Are there any user-facing changes?

Multi-column IN / NOT IN subqueries are now supported.

API change: Join and HashJoinExec have a new public null_aware_value_keys field, and the generated protobuf JoinNode and HashJoinExecNode have a new field. Code that builds these with struct literals must set it (1 keeps the previous behavior). The 56.0.0 upgrade guide describes the change.

@viirya viirya changed the title Multi column null aware anti join feat: Add multi-column support for null-aware anti joins Jan 16, 2026
@github-actions github-actions Bot added sql SQL Planner logical-expr Logical plan and expressions optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt) physical-plan Changes to the physical-plan crate labels Jan 16, 2026
@viirya
viirya force-pushed the multi-column-null-aware-anti-join branch 2 times, most recently from f61eabb to f6db769 Compare January 16, 2026 22:25
@viirya
viirya force-pushed the multi-column-null-aware-anti-join branch from d07982f to 5662114 Compare February 8, 2026 22:26
@viirya
viirya requested review from alamb and comphead February 8, 2026 22:27
@comphead

Copy link
Copy Markdown
Contributor

Thanks @viirya I'll check it soon, afaik there is no TPC* like queries that cover multiple column null aware anti joins, so it would be prob nice to have a bench in future to make sure no performance regression introduced with future PRs

@comphead

Copy link
Copy Markdown
Contributor

@2010YOUY01 @jonathanc-n FYI

@viirya
viirya force-pushed the multi-column-null-aware-anti-join branch from 5662114 to 6613da5 Compare March 26, 2026 07:19
@Dandandan

Dandandan commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

run benchmark tpcds

@adriangbot

Copy link
Copy Markdown

🤖 Criterion benchmark running (GKE) | trigger
Linux bench-c4132580418-557-qcnt8 6.12.55+ #1 SMP Sun Feb 1 08:59:41 UTC 2026 aarch64 GNU/Linux
Comparing multi-column-null-aware-anti-join (6613da5) to 4e2e4e8 (merge-base) diff
BENCH_NAME=tpc-ds
BENCH_COMMAND=cargo bench --features=parquet --bench tpc-ds
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed.

Last 20 lines of output:

Click to expand
    stats_merge
    strpos
    struct_query_sql
    substr
    substr_index
    substring
    sum
    to_char
    to_hex
    to_timestamp
    topk_aggregate
    topk_repartition
    translate
    trim
    trunc
    unhex
    upper
    uuid
    window_query_sql
    with_hashes

File an issue against this benchmark runner

@Dandandan

Copy link
Copy Markdown
Contributor

run benchmark tpcds

@Dandandan

Copy link
Copy Markdown
Contributor

Not sure if tpcds contains NA Anti Join, but it at least contains many join types

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Linux bench-c4132611332-558-9q4bg 6.12.55+ #1 SMP Sun Feb 1 08:59:41 UTC 2026 aarch64 GNU/Linux
Comparing multi-column-null-aware-anti-join (6613da5) to 4e2e4e8 (merge-base) diff using: tpcds
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Details

Comparing HEAD and multi-column-null-aware-anti-join
--------------------
Benchmark tpcds_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃                                     HEAD ┃        multi-column-null-aware-anti-join ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 1  │           43.83 / 44.48 ±0.78 / 45.74 ms │           43.31 / 44.09 ±0.73 / 45.47 ms │     no change │
│ QQuery 2  │        146.21 / 147.16 ±0.77 / 148.28 ms │        147.17 / 147.48 ±0.36 / 147.92 ms │     no change │
│ QQuery 3  │        116.32 / 117.06 ±0.54 / 117.70 ms │        115.53 / 116.25 ±0.53 / 117.10 ms │     no change │
│ QQuery 4  │     1318.18 / 1335.72 ±8.89 / 1342.24 ms │    1302.77 / 1339.59 ±27.16 / 1375.30 ms │     no change │
│ QQuery 5  │        173.39 / 175.18 ±1.71 / 177.99 ms │        175.20 / 176.42 ±1.06 / 178.04 ms │     no change │
│ QQuery 6  │     976.93 / 1017.94 ±25.92 / 1054.05 ms │     983.14 / 1014.13 ±27.67 / 1053.44 ms │     no change │
│ QQuery 7  │        354.18 / 357.36 ±2.47 / 361.50 ms │        352.61 / 356.67 ±3.24 / 359.99 ms │     no change │
│ QQuery 8  │        116.72 / 117.72 ±1.19 / 120.07 ms │        115.87 / 116.81 ±0.74 / 117.72 ms │     no change │
│ QQuery 9  │        102.03 / 105.59 ±2.94 / 108.74 ms │        107.50 / 112.35 ±8.61 / 129.55 ms │  1.06x slower │
│ QQuery 10 │        108.19 / 109.89 ±1.06 / 110.86 ms │        107.98 / 109.25 ±0.69 / 109.88 ms │     no change │
│ QQuery 11 │       908.88 / 932.51 ±15.90 / 956.37 ms │       885.34 / 923.46 ±27.79 / 960.12 ms │     no change │
│ QQuery 12 │           46.06 / 47.12 ±0.70 / 48.11 ms │           45.21 / 45.71 ±0.39 / 46.38 ms │     no change │
│ QQuery 13 │        403.29 / 407.02 ±2.30 / 410.46 ms │        400.29 / 403.12 ±3.05 / 408.99 ms │     no change │
│ QQuery 14 │     1029.55 / 1041.76 ±8.13 / 1052.07 ms │     1027.32 / 1035.48 ±6.46 / 1041.60 ms │     no change │
│ QQuery 15 │           16.56 / 17.29 ±0.53 / 18.11 ms │           15.73 / 16.96 ±0.86 / 17.87 ms │     no change │
│ QQuery 16 │           41.90 / 42.93 ±0.73 / 44.12 ms │           41.38 / 42.25 ±0.92 / 43.90 ms │     no change │
│ QQuery 17 │        245.22 / 247.74 ±1.53 / 249.40 ms │        239.64 / 240.72 ±0.66 / 241.59 ms │     no change │
│ QQuery 18 │        131.28 / 133.03 ±1.12 / 134.30 ms │        129.15 / 130.47 ±0.95 / 131.92 ms │     no change │
│ QQuery 19 │        158.07 / 159.11 ±1.04 / 161.08 ms │        156.74 / 158.82 ±1.44 / 160.64 ms │     no change │
│ QQuery 20 │           14.21 / 15.37 ±0.77 / 16.32 ms │           13.80 / 14.39 ±0.61 / 15.46 ms │ +1.07x faster │
│ QQuery 21 │           20.81 / 21.12 ±0.22 / 21.37 ms │           19.99 / 20.26 ±0.29 / 20.80 ms │     no change │
│ QQuery 22 │        493.95 / 496.55 ±1.48 / 498.39 ms │        487.27 / 491.13 ±3.32 / 496.07 ms │     no change │
│ QQuery 23 │        916.18 / 922.08 ±6.99 / 935.23 ms │        896.75 / 908.58 ±8.32 / 918.52 ms │     no change │
│ QQuery 24 │        418.66 / 421.78 ±2.93 / 427.12 ms │        420.02 / 423.04 ±3.35 / 429.56 ms │     no change │
│ QQuery 25 │        354.94 / 360.65 ±3.68 / 365.79 ms │        358.66 / 360.03 ±0.90 / 361.03 ms │     no change │
│ QQuery 26 │           84.89 / 85.93 ±0.79 / 87.09 ms │           82.96 / 84.51 ±1.72 / 87.71 ms │     no change │
│ QQuery 27 │        351.74 / 354.72 ±3.43 / 360.26 ms │        347.83 / 352.22 ±2.92 / 355.67 ms │     no change │
│ QQuery 28 │        150.47 / 153.64 ±1.73 / 155.63 ms │        150.58 / 151.68 ±1.09 / 153.25 ms │     no change │
│ QQuery 29 │        304.54 / 308.63 ±3.86 / 314.17 ms │        299.22 / 302.51 ±3.00 / 307.86 ms │     no change │
│ QQuery 30 │           43.75 / 46.65 ±2.47 / 50.74 ms │           43.52 / 45.83 ±1.63 / 48.58 ms │     no change │
│ QQuery 31 │        171.49 / 174.14 ±1.77 / 176.36 ms │        171.63 / 174.30 ±1.66 / 176.08 ms │     no change │
│ QQuery 32 │         57.66 / 67.54 ±17.15 / 101.79 ms │           57.71 / 59.09 ±1.14 / 60.54 ms │ +1.14x faster │
│ QQuery 33 │        144.20 / 145.25 ±0.76 / 146.50 ms │        142.15 / 144.13 ±1.39 / 145.86 ms │     no change │
│ QQuery 34 │        108.82 / 109.88 ±1.04 / 111.73 ms │        107.76 / 108.62 ±0.72 / 109.75 ms │     no change │
│ QQuery 35 │        110.99 / 112.62 ±0.87 / 113.41 ms │        110.10 / 111.19 ±1.11 / 113.20 ms │     no change │
│ QQuery 36 │        217.46 / 224.74 ±4.61 / 230.43 ms │        216.54 / 220.79 ±3.76 / 225.36 ms │     no change │
│ QQuery 37 │        180.41 / 184.25 ±2.30 / 186.85 ms │        179.45 / 181.42 ±1.34 / 183.52 ms │     no change │
│ QQuery 38 │           89.32 / 91.84 ±2.78 / 96.01 ms │           85.64 / 89.98 ±3.81 / 96.51 ms │     no change │
│ QQuery 39 │        124.50 / 130.42 ±3.89 / 136.67 ms │        129.92 / 132.65 ±2.37 / 135.43 ms │     no change │
│ QQuery 40 │        114.26 / 119.79 ±5.41 / 129.80 ms │        116.16 / 119.54 ±5.34 / 130.15 ms │     no change │
│ QQuery 41 │           14.74 / 15.91 ±0.71 / 16.79 ms │           14.58 / 15.96 ±1.05 / 17.31 ms │     no change │
│ QQuery 42 │        108.67 / 110.20 ±0.85 / 111.07 ms │        109.11 / 110.81 ±0.97 / 112.00 ms │     no change │
│ QQuery 43 │           84.01 / 85.47 ±0.90 / 86.82 ms │           84.92 / 86.07 ±1.05 / 87.41 ms │     no change │
│ QQuery 44 │           12.32 / 12.48 ±0.17 / 12.81 ms │           11.45 / 11.86 ±0.32 / 12.33 ms │ +1.05x faster │
│ QQuery 45 │           53.98 / 55.49 ±0.89 / 56.65 ms │           52.75 / 54.47 ±1.14 / 56.20 ms │     no change │
│ QQuery 46 │        236.05 / 238.07 ±1.14 / 239.41 ms │        232.13 / 235.67 ±2.35 / 238.90 ms │     no change │
│ QQuery 47 │       693.34 / 718.06 ±16.10 / 741.93 ms │        691.86 / 704.19 ±8.70 / 714.81 ms │     no change │
│ QQuery 48 │        289.45 / 295.56 ±5.22 / 302.94 ms │        291.82 / 293.85 ±3.10 / 300.00 ms │     no change │
│ QQuery 49 │        256.17 / 259.32 ±2.88 / 263.76 ms │        254.94 / 256.85 ±2.36 / 260.84 ms │     no change │
│ QQuery 50 │        236.05 / 238.86 ±3.51 / 245.00 ms │        231.86 / 234.50 ±2.35 / 237.93 ms │     no change │
│ QQuery 51 │        183.08 / 189.42 ±4.47 / 195.93 ms │        188.83 / 191.15 ±2.02 / 193.76 ms │     no change │
│ QQuery 52 │        108.99 / 110.06 ±0.66 / 111.04 ms │        110.09 / 111.24 ±0.96 / 112.84 ms │     no change │
│ QQuery 53 │        103.49 / 104.18 ±0.63 / 105.31 ms │        105.87 / 107.09 ±0.90 / 108.56 ms │     no change │
│ QQuery 54 │        148.12 / 149.81 ±1.88 / 153.33 ms │        150.72 / 151.89 ±1.56 / 154.69 ms │     no change │
│ QQuery 55 │        108.98 / 111.00 ±1.87 / 113.69 ms │        109.27 / 110.39 ±1.01 / 112.28 ms │     no change │
│ QQuery 56 │        143.43 / 145.10 ±1.68 / 148.03 ms │        144.87 / 145.69 ±0.56 / 146.43 ms │     no change │
│ QQuery 57 │        178.73 / 180.46 ±1.51 / 182.61 ms │        178.24 / 179.32 ±0.93 / 181.06 ms │     no change │
│ QQuery 58 │       295.59 / 308.05 ±11.94 / 324.77 ms │        297.21 / 305.11 ±6.36 / 314.08 ms │     no change │
│ QQuery 59 │        197.88 / 200.82 ±2.19 / 203.82 ms │        203.20 / 204.87 ±0.88 / 205.66 ms │     no change │
│ QQuery 60 │        144.17 / 146.66 ±1.54 / 148.48 ms │        148.65 / 149.86 ±0.81 / 150.79 ms │     no change │
│ QQuery 61 │        173.03 / 174.47 ±0.82 / 175.26 ms │        174.07 / 175.60 ±1.08 / 177.31 ms │     no change │
│ QQuery 62 │        928.31 / 940.78 ±8.68 / 951.41 ms │       881.70 / 920.22 ±31.31 / 970.43 ms │     no change │
│ QQuery 63 │        105.06 / 107.54 ±2.52 / 111.73 ms │        106.19 / 108.64 ±1.60 / 110.49 ms │     no change │
│ QQuery 64 │        714.18 / 717.77 ±3.02 / 722.59 ms │        702.07 / 705.06 ±3.71 / 711.51 ms │     no change │
│ QQuery 65 │        257.73 / 260.58 ±2.32 / 263.22 ms │        253.11 / 256.56 ±3.22 / 262.46 ms │     no change │
│ QQuery 66 │        248.65 / 256.39 ±5.54 / 262.34 ms │        243.88 / 253.67 ±8.19 / 263.92 ms │     no change │
│ QQuery 67 │        319.17 / 321.87 ±2.96 / 327.32 ms │        314.63 / 317.13 ±2.10 / 321.03 ms │     no change │
│ QQuery 68 │        282.59 / 287.94 ±4.19 / 294.81 ms │        281.82 / 286.47 ±3.05 / 290.84 ms │     no change │
│ QQuery 69 │        104.85 / 107.06 ±3.00 / 112.91 ms │        103.82 / 106.11 ±1.48 / 108.00 ms │     no change │
│ QQuery 70 │       336.33 / 352.78 ±12.21 / 373.19 ms │       329.23 / 339.61 ±12.22 / 363.38 ms │     no change │
│ QQuery 71 │        137.31 / 138.16 ±0.90 / 139.81 ms │        134.47 / 136.82 ±1.88 / 139.88 ms │     no change │
│ QQuery 72 │       714.00 / 735.13 ±23.66 / 780.30 ms │       711.30 / 735.79 ±14.74 / 751.21 ms │     no change │
│ QQuery 73 │        103.18 / 106.39 ±2.30 / 109.20 ms │        105.75 / 108.36 ±3.10 / 113.66 ms │     no change │
│ QQuery 74 │        579.47 / 592.96 ±9.58 / 605.62 ms │       574.38 / 622.50 ±29.78 / 654.64 ms │     no change │
│ QQuery 75 │        278.60 / 282.13 ±1.83 / 283.80 ms │        282.42 / 289.49 ±3.84 / 293.37 ms │     no change │
│ QQuery 76 │        133.09 / 134.84 ±1.42 / 137.33 ms │        138.55 / 139.19 ±0.48 / 139.90 ms │     no change │
│ QQuery 77 │        188.52 / 192.32 ±2.32 / 195.51 ms │        189.12 / 192.93 ±2.10 / 194.90 ms │     no change │
│ QQuery 78 │        359.65 / 364.01 ±3.02 / 366.75 ms │        348.34 / 354.51 ±4.27 / 360.49 ms │     no change │
│ QQuery 79 │        245.19 / 249.52 ±2.98 / 252.64 ms │        236.97 / 237.98 ±0.98 / 239.73 ms │     no change │
│ QQuery 80 │        331.90 / 337.17 ±3.40 / 342.21 ms │        330.97 / 332.74 ±1.02 / 334.08 ms │     no change │
│ QQuery 81 │           28.66 / 30.18 ±1.62 / 33.20 ms │           26.16 / 27.56 ±1.21 / 29.12 ms │ +1.09x faster │
│ QQuery 82 │        203.91 / 207.17 ±2.42 / 210.21 ms │        203.99 / 205.52 ±1.48 / 207.82 ms │     no change │
│ QQuery 83 │           40.55 / 42.37 ±1.34 / 43.99 ms │           38.40 / 40.75 ±1.44 / 42.86 ms │     no change │
│ QQuery 84 │           49.62 / 50.86 ±1.08 / 52.18 ms │           49.13 / 49.80 ±0.48 / 50.55 ms │     no change │
│ QQuery 85 │        153.00 / 154.47 ±1.35 / 156.72 ms │        149.41 / 150.98 ±1.67 / 154.22 ms │     no change │
│ QQuery 86 │           40.79 / 42.04 ±0.97 / 43.45 ms │           38.25 / 39.60 ±1.31 / 41.28 ms │ +1.06x faster │
│ QQuery 87 │           86.64 / 90.50 ±3.92 / 97.56 ms │           84.53 / 88.29 ±3.39 / 94.69 ms │     no change │
│ QQuery 88 │        101.02 / 101.80 ±0.79 / 103.12 ms │        100.51 / 101.92 ±0.84 / 102.77 ms │     no change │
│ QQuery 89 │        120.00 / 121.30 ±1.05 / 122.53 ms │        119.10 / 120.92 ±1.16 / 122.61 ms │     no change │
│ QQuery 90 │           23.69 / 24.39 ±0.46 / 25.07 ms │           23.96 / 24.79 ±0.62 / 25.70 ms │     no change │
│ QQuery 91 │           64.94 / 66.92 ±1.16 / 68.09 ms │           62.85 / 64.59 ±1.07 / 65.64 ms │     no change │
│ QQuery 92 │           58.27 / 59.03 ±0.85 / 60.50 ms │           58.12 / 58.99 ±0.78 / 59.94 ms │     no change │
│ QQuery 93 │        189.68 / 192.58 ±2.47 / 197.13 ms │        189.91 / 191.49 ±0.92 / 192.67 ms │     no change │
│ QQuery 94 │           62.20 / 62.74 ±0.59 / 63.83 ms │           60.69 / 61.84 ±0.62 / 62.55 ms │     no change │
│ QQuery 95 │        133.81 / 137.41 ±1.99 / 139.20 ms │        133.63 / 135.07 ±1.05 / 136.54 ms │     no change │
│ QQuery 96 │           73.80 / 75.51 ±1.23 / 77.54 ms │           76.32 / 76.87 ±0.31 / 77.22 ms │     no change │
│ QQuery 97 │        130.18 / 133.14 ±2.37 / 136.33 ms │        130.53 / 135.82 ±2.77 / 138.65 ms │     no change │
│ QQuery 98 │        156.33 / 157.92 ±1.18 / 159.89 ms │        152.35 / 157.35 ±3.55 / 161.90 ms │     no change │
│ QQuery 99 │ 10713.08 / 10785.09 ±43.20 / 10843.97 ms │ 10730.66 / 10761.22 ±23.32 / 10798.69 ms │     no change │
└───────────┴──────────────────────────────────────────┴──────────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                                ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                                │ 33974.38ms │
│ Total Time (multi-column-null-aware-anti-join)   │ 33799.52ms │
│ Average Time (HEAD)                              │   343.18ms │
│ Average Time (multi-column-null-aware-anti-join) │   341.41ms │
│ Queries Faster                                   │          5 │
│ Queries Slower                                   │          1 │
│ Queries with No Change                           │         93 │
│ Queries with Failure                             │          0 │
└──────────────────────────────────────────────────┴────────────┘

Resource Usage

tpcds — base (merge-base)

Metric Value
Wall time 170.2s
Peak memory 5.8 GiB
Avg memory 4.7 GiB
CPU user 273.4s
CPU sys 19.0s
Disk read 0 B
Disk write 702.8 MiB

tpcds — branch

Metric Value
Wall time 169.3s
Peak memory 5.9 GiB
Avg memory 4.7 GiB
CPU user 272.5s
CPU sys 18.4s
Disk read 0 B
Disk write 148.0 KiB

File an issue against this benchmark runner

@viirya
viirya force-pushed the multi-column-null-aware-anti-join branch from 9510be2 to 94265ba Compare April 8, 2026 06:08
Comment thread datafusion/sqllogictest/test_files/null_aware_anti_join.slt Outdated
@viirya

viirya commented Apr 17, 2026 •

Copy link
Copy Markdown
Member Author

I cross-validated all test cases (Test 1–25) against Postgres 16 running in a Docker container. I translated each .slt test into equivalent Postgres SQL and compared the output row-by-row against DataFusion's expected results. All 25 tests matched Postgres exactly, covering single-column NOT IN (Tests 1–18), multi-column NOT IN (Tests 19–23), correlated multi-column NOT IN (Test 24), and empty subquery (Test 25).

@viirya
viirya force-pushed the multi-column-null-aware-anti-join branch from 1f579fd to d672544 Compare May 3, 2026 07:18
@viirya
viirya force-pushed the multi-column-null-aware-anti-join branch from d672544 to 0b25fde Compare June 21, 2026 00:20
@github-actions

Copy link
Copy Markdown

Thank you for your contribution. Unfortunately, this pull request is stale because it has been open 60 days with no activity. Please remove the stale label or comment or this will be closed in 7 days.

@github-actions github-actions Bot added the Stale PR has not had any activity for some time label Aug 20, 2026
@viirya viirya removed the Stale PR has not had any activity for some time label Aug 20, 2026
@comphead

Copy link
Copy Markdown
Contributor

run benchmark tpch tpcds

@viirya

viirya commented Sep 30, 2026

Copy link
Copy Markdown
Member Author

I will resolve the conflicts.

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5901079308-2930-gp676 6.12.94+ #1 SMP Wed Aug 19 07:47:20 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 multi-column-null-aware-anti-join (295c056) to 696eaf5 (merge-base) diff

Run configuration
run benchmark tpch

Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5901079308-2931-rwznz 6.12.94+ #1 SMP Wed Aug 19 07:47:20 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 multi-column-null-aware-anti-join (295c056) to 696eaf5 (merge-base) diff

Run configuration
run benchmark tpcds

Results will be posted here when complete


File an issue against this benchmark runner

# Conflicts:
#	datafusion/optimizer/src/decorrelate_predicate_subquery.rs
#	datafusion/physical-plan/src/joins/hash_join/exec.rs
#	datafusion/sqllogictest/test_files/subquery.slt
#	docs/source/library-user-guide/upgrading/56.0.0.md
@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

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

Comparing multi-column-null-aware-anti-join (295c056) to 696eaf5 (merge-base) diff

Run configuration
run benchmark tpch
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 multi-column-null-aware-anti-join
--------------------
Benchmark tpch_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query     ┃     HEAD ┃ multi-column-null-aware-anti-join ┃    Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ QQuery 1  │ 40.63 ms │                          40.76 ms │ no change │
│ QQuery 2  │ 18.35 ms │                          18.46 ms │ no change │
│ QQuery 3  │ 28.21 ms │                          28.34 ms │ no change │
│ QQuery 4  │ 17.18 ms │                          17.19 ms │ no change │
│ QQuery 5  │ 34.90 ms │                          35.08 ms │ no change │
│ QQuery 6  │ 15.97 ms │                          16.34 ms │ no change │
│ QQuery 7  │ 40.62 ms │                          41.84 ms │ no change │
│ QQuery 8  │ 39.99 ms │                          40.23 ms │ no change │
│ QQuery 9  │ 49.21 ms │                          48.72 ms │ no change │
│ QQuery 10 │ 40.26 ms │                          40.15 ms │ no change │
│ QQuery 11 │ 13.55 ms │                          13.28 ms │ no change │
│ QQuery 12 │ 20.79 ms │                          20.77 ms │ no change │
│ QQuery 13 │ 39.38 ms │                          39.28 ms │ no change │
│ QQuery 14 │ 24.47 ms │                          24.32 ms │ no change │
│ QQuery 15 │ 30.85 ms │                          30.60 ms │ no change │
│ QQuery 16 │ 13.81 ms │                          13.94 ms │ no change │
│ QQuery 17 │ 71.80 ms │                          71.95 ms │ no change │
│ QQuery 18 │ 62.01 ms │                          61.48 ms │ no change │
│ QQuery 19 │ 31.63 ms │                          31.84 ms │ no change │
│ QQuery 20 │ 31.36 ms │                          31.51 ms │ no change │
│ QQuery 21 │ 54.66 ms │                          54.40 ms │ no change │
│ QQuery 22 │ 14.31 ms │                          13.65 ms │ no change │
└───────────┴──────────┴───────────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Benchmark Summary                                ┃          ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Total Time (HEAD)                                │ 733.94ms │
│ Total Time (multi-column-null-aware-anti-join)   │ 734.14ms │
│ Average Time (HEAD)                              │  33.36ms │
│ Average Time (multi-column-null-aware-anti-join) │  33.37ms │
│ Queries Faster                                   │        0 │
│ Queries Slower                                   │        0 │
│ Queries with No Change                           │       22 │
│ Queries with Failure                             │        0 │
└──────────────────────────────────────────────────┴──────────┘

Distribution per query (min / mean ±stddev / max):

Comparing HEAD and multi-column-null-aware-anti-join
--------------------
Benchmark tpch_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query     ┃                           HEAD ┃ multi-column-null-aware-anti-join ┃    Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ QQuery 1  │ 40.63 / 42.74 ±2.56 / 46.49 ms │    40.76 / 41.46 ±1.06 / 43.57 ms │ no change │
│ QQuery 2  │ 18.35 / 18.67 ±0.22 / 18.99 ms │    18.46 / 19.00 ±0.65 / 20.25 ms │ no change │
│ QQuery 3  │ 28.21 / 28.61 ±0.23 / 28.87 ms │    28.34 / 28.50 ±0.12 / 28.65 ms │ no change │
│ QQuery 4  │ 17.18 / 17.98 ±0.81 / 19.11 ms │    17.19 / 17.49 ±0.33 / 18.12 ms │ no change │
│ QQuery 5  │ 34.90 / 35.08 ±0.12 / 35.21 ms │    35.08 / 36.09 ±1.42 / 38.83 ms │ no change │
│ QQuery 6  │ 15.97 / 16.45 ±0.51 / 17.37 ms │    16.34 / 16.57 ±0.16 / 16.74 ms │ no change │
│ QQuery 7  │ 40.62 / 41.57 ±1.07 / 43.55 ms │    41.84 / 43.13 ±0.94 / 44.24 ms │ no change │
│ QQuery 8  │ 39.99 / 40.61 ±0.37 / 40.99 ms │    40.23 / 40.87 ±0.89 / 42.63 ms │ no change │
│ QQuery 9  │ 49.21 / 49.90 ±0.73 / 51.22 ms │    48.72 / 50.22 ±0.89 / 51.31 ms │ no change │
│ QQuery 10 │ 40.26 / 41.38 ±1.08 / 42.89 ms │    40.15 / 40.51 ±0.22 / 40.79 ms │ no change │
│ QQuery 11 │ 13.55 / 14.01 ±0.70 / 15.41 ms │    13.28 / 13.57 ±0.22 / 13.89 ms │ no change │
│ QQuery 12 │ 20.79 / 21.22 ±0.26 / 21.55 ms │    20.77 / 21.42 ±0.60 / 22.46 ms │ no change │
│ QQuery 13 │ 39.38 / 41.15 ±2.01 / 44.64 ms │    39.28 / 40.45 ±1.27 / 42.70 ms │ no change │
│ QQuery 14 │ 24.47 / 24.73 ±0.14 / 24.86 ms │    24.32 / 24.51 ±0.13 / 24.69 ms │ no change │
│ QQuery 15 │ 30.85 / 31.41 ±0.61 / 32.54 ms │    30.60 / 31.04 ±0.56 / 32.14 ms │ no change │
│ QQuery 16 │ 13.81 / 14.15 ±0.30 / 14.68 ms │    13.94 / 14.32 ±0.37 / 15.02 ms │ no change │
│ QQuery 17 │ 71.80 / 73.64 ±1.27 / 75.57 ms │    71.95 / 73.85 ±1.48 / 75.63 ms │ no change │
│ QQuery 18 │ 62.01 / 64.38 ±1.89 / 67.03 ms │    61.48 / 62.25 ±0.53 / 62.83 ms │ no change │
│ QQuery 19 │ 31.63 / 33.70 ±1.41 / 35.67 ms │    31.84 / 32.48 ±0.94 / 34.35 ms │ no change │
│ QQuery 20 │ 31.36 / 31.78 ±0.28 / 32.19 ms │    31.51 / 31.73 ±0.17 / 32.02 ms │ no change │
│ QQuery 21 │ 54.66 / 55.53 ±0.68 / 56.35 ms │    54.40 / 55.13 ±0.78 / 56.64 ms │ no change │
│ QQuery 22 │ 14.31 / 14.37 ±0.04 / 14.43 ms │    13.65 / 13.89 ±0.17 / 14.08 ms │ no change │
└───────────┴────────────────────────────────┴───────────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Benchmark Summary                                ┃          ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Total Time (HEAD)                                │ 753.07ms │
│ Total Time (multi-column-null-aware-anti-join)   │ 748.48ms │
│ Average Time (HEAD)                              │  34.23ms │
│ Average Time (multi-column-null-aware-anti-join) │  34.02ms │
│ 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 506.3 MiB
CPU user 20.9s
CPU sys 1.7s
Peak spill 0 B

tpch — branch

Metric Value
Wall time 5.0s
Peak memory 1.3 GiB
Avg memory 495.3 MiB
CPU user 20.9s
CPU sys 1.6s
Peak spill 0 B

File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

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

Comparing multi-column-null-aware-anti-join (295c056) to 696eaf5 (merge-base) diff

Run configuration
run benchmark tpcds
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 multi-column-null-aware-anti-join
--------------------
Benchmark tpcds_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Query     ┃       HEAD ┃ multi-column-null-aware-anti-join ┃       Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ QQuery 1  │    5.58 ms │                           5.90 ms │ 1.06x slower │
│ QQuery 2  │   80.94 ms │                          80.60 ms │    no change │
│ QQuery 3  │   29.18 ms │                          28.97 ms │    no change │
│ QQuery 4  │  466.81 ms │                         473.23 ms │    no change │
│ QQuery 5  │   52.03 ms │                          51.74 ms │    no change │
│ QQuery 6  │   35.96 ms │                          36.27 ms │    no change │
│ QQuery 7  │   73.96 ms │                          74.09 ms │    no change │
│ QQuery 8  │   36.49 ms │                          36.46 ms │    no change │
│ QQuery 9  │   52.55 ms │                          53.94 ms │    no change │
│ QQuery 10 │   61.98 ms │                          61.40 ms │    no change │
│ QQuery 11 │  293.06 ms │                         291.31 ms │    no change │
│ QQuery 12 │   29.23 ms │                          28.96 ms │    no change │
│ QQuery 13 │  117.84 ms │                         117.83 ms │    no change │
│ QQuery 14 │  396.28 ms │                         394.55 ms │    no change │
│ QQuery 15 │   56.78 ms │                          55.99 ms │    no change │
│ QQuery 16 │    6.67 ms │                           6.49 ms │    no change │
│ QQuery 17 │   79.23 ms │                          78.13 ms │    no change │
│ QQuery 18 │  104.93 ms │                         104.74 ms │    no change │
│ QQuery 19 │   41.28 ms │                          42.46 ms │    no change │
│ QQuery 20 │   36.04 ms │                          35.81 ms │    no change │
│ QQuery 21 │   17.34 ms │                          17.51 ms │    no change │
│ QQuery 22 │   64.50 ms │                          65.25 ms │    no change │
│ QQuery 23 │  321.07 ms │                         318.59 ms │    no change │
│ QQuery 24 │  199.28 ms │                         199.22 ms │    no change │
│ QQuery 25 │  107.34 ms │                         108.21 ms │    no change │
│ QQuery 26 │   48.47 ms │                          48.72 ms │    no change │
│ QQuery 27 │    6.36 ms │                           6.28 ms │    no change │
│ QQuery 28 │   56.51 ms │                          58.45 ms │    no change │
│ QQuery 29 │   95.18 ms │                          95.62 ms │    no change │
│ QQuery 30 │   32.99 ms │                          33.33 ms │    no change │
│ QQuery 31 │  109.78 ms │                         110.59 ms │    no change │
│ QQuery 32 │   20.87 ms │                          20.91 ms │    no change │
│ QQuery 33 │   37.47 ms │                          37.69 ms │    no change │
│ QQuery 34 │   10.38 ms │                          10.34 ms │    no change │
│ QQuery 35 │   73.65 ms │                          74.65 ms │    no change │
│ QQuery 36 │    6.16 ms │                           5.85 ms │    no change │
│ QQuery 37 │    7.08 ms │                           7.11 ms │    no change │
│ QQuery 38 │   63.21 ms │                          63.88 ms │    no change │
│ QQuery 39 │   75.03 ms │                          75.95 ms │    no change │
│ QQuery 40 │   24.83 ms │                          24.21 ms │    no change │
│ QQuery 41 │   11.86 ms │                          11.90 ms │    no change │
│ QQuery 42 │   24.10 ms │                          24.09 ms │    no change │
│ QQuery 43 │    5.37 ms │                           5.31 ms │    no change │
│ QQuery 44 │    9.55 ms │                           9.62 ms │    no change │
│ QQuery 45 │   40.12 ms │                          40.12 ms │    no change │
│ QQuery 46 │   12.20 ms │                          12.49 ms │    no change │
│ QQuery 47 │  232.51 ms │                         230.78 ms │    no change │
│ QQuery 48 │   95.86 ms │                          95.84 ms │    no change │
│ QQuery 49 │   71.10 ms │                          70.79 ms │    no change │
│ QQuery 50 │   58.59 ms │                          58.08 ms │    no change │
│ QQuery 51 │   93.28 ms │                          92.52 ms │    no change │
│ QQuery 52 │   24.41 ms │                          26.13 ms │ 1.07x slower │
│ QQuery 53 │   29.49 ms │                          29.38 ms │    no change │
│ QQuery 54 │   54.60 ms │                          54.51 ms │    no change │
│ QQuery 55 │   23.75 ms │                          23.79 ms │    no change │
│ QQuery 56 │   39.10 ms │                          38.43 ms │    no change │
│ QQuery 57 │  169.17 ms │                         169.69 ms │    no change │
│ QQuery 58 │  109.97 ms │                         109.47 ms │    no change │
│ QQuery 59 │  116.72 ms │                         116.51 ms │    no change │
│ QQuery 60 │   39.31 ms │                          39.19 ms │    no change │
│ QQuery 61 │   11.87 ms │                          11.97 ms │    no change │
│ QQuery 62 │   44.68 ms │                          45.24 ms │    no change │
│ QQuery 63 │   29.65 ms │                          29.81 ms │    no change │
│ QQuery 64 │  365.35 ms │                         368.69 ms │    no change │
│ QQuery 65 │  130.61 ms │                         132.23 ms │    no change │
│ QQuery 66 │   77.36 ms │                          77.88 ms │    no change │
│ QQuery 67 │  258.25 ms │                         260.60 ms │    no change │
│ QQuery 68 │   12.24 ms │                          12.20 ms │    no change │
│ QQuery 69 │   57.20 ms │                          57.48 ms │    no change │
│ QQuery 70 │  104.55 ms │                         105.89 ms │    no change │
│ QQuery 71 │   35.53 ms │                          35.43 ms │    no change │
│ QQuery 72 │ 1830.92 ms │                        1814.12 ms │    no change │
│ QQuery 73 │   10.18 ms │                          10.17 ms │    no change │
│ QQuery 74 │  169.50 ms │                         174.00 ms │    no change │
│ QQuery 75 │  142.65 ms │                         143.38 ms │    no change │
│ QQuery 76 │   35.39 ms │                          34.16 ms │    no change │
│ QQuery 77 │   60.90 ms │                          62.20 ms │    no change │
│ QQuery 78 │  163.97 ms │                         164.63 ms │    no change │
│ QQuery 79 │   66.77 ms │                          67.05 ms │    no change │
│ QQuery 80 │   97.80 ms │                          96.14 ms │    no change │
│ QQuery 81 │   26.62 ms │                          26.37 ms │    no change │
│ QQuery 82 │   16.91 ms │                          17.23 ms │    no change │
│ QQuery 83 │   34.27 ms │                          34.29 ms │    no change │
│ QQuery 84 │   29.97 ms │                          30.31 ms │    no change │
│ QQuery 85 │  104.39 ms │                         103.66 ms │    no change │
│ QQuery 86 │   26.26 ms │                          26.00 ms │    no change │
│ QQuery 87 │   63.91 ms │                          64.30 ms │    no change │
│ QQuery 88 │   61.73 ms │                          62.09 ms │    no change │
│ QQuery 89 │   36.05 ms │                          35.63 ms │    no change │
│ QQuery 90 │   17.13 ms │                          17.14 ms │    no change │
│ QQuery 91 │   44.74 ms │                          45.93 ms │    no change │
│ QQuery 92 │   29.98 ms │                          30.78 ms │    no change │
│ QQuery 93 │   49.44 ms │                          49.18 ms │    no change │
│ QQuery 94 │   39.13 ms │                          38.98 ms │    no change │
│ QQuery 95 │   80.22 ms │                          79.99 ms │    no change │
│ QQuery 96 │   24.21 ms │                          24.09 ms │    no change │
│ QQuery 97 │   51.17 ms │                          51.36 ms │    no change │
│ QQuery 98 │   43.37 ms │                          43.74 ms │    no change │
│ QQuery 99 │   65.90 ms │                          66.42 ms │    no change │
└───────────┴────────────┴───────────────────────────────────┴──────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary                                ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (HEAD)                                │ 9246.15ms │
│ Total Time (multi-column-null-aware-anti-join)   │ 9250.60ms │
│ Average Time (HEAD)                              │   93.40ms │
│ Average Time (multi-column-null-aware-anti-join) │   93.44ms │
│ Queries Faster                                   │         0 │
│ Queries Slower                                   │         2 │
│ Queries with No Change                           │        97 │
│ Queries with Failure                             │         0 │
└──────────────────────────────────────────────────┴───────────┘

Distribution per query (min / mean ±stddev / max):

Comparing HEAD and multi-column-null-aware-anti-join
--------------------
Benchmark tpcds_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃                                  HEAD ┃     multi-column-null-aware-anti-join ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 1  │           5.58 / 6.24 ±0.96 / 8.14 ms │           5.90 / 6.39 ±0.85 / 8.09 ms │     no change │
│ QQuery 2  │        80.94 / 81.19 ±0.17 / 81.37 ms │        80.60 / 81.05 ±0.31 / 81.37 ms │     no change │
│ QQuery 3  │        29.18 / 29.35 ±0.14 / 29.55 ms │        28.97 / 29.34 ±0.29 / 29.61 ms │     no change │
│ QQuery 4  │    466.81 / 476.10 ±11.68 / 498.95 ms │     473.23 / 475.47 ±1.23 / 476.80 ms │     no change │
│ QQuery 5  │        52.03 / 53.57 ±2.59 / 58.72 ms │        51.74 / 53.72 ±2.79 / 59.10 ms │     no change │
│ QQuery 6  │        35.96 / 36.69 ±0.40 / 37.12 ms │        36.27 / 36.48 ±0.22 / 36.87 ms │     no change │
│ QQuery 7  │        73.96 / 74.96 ±0.71 / 75.80 ms │        74.09 / 74.59 ±0.33 / 75.05 ms │     no change │
│ QQuery 8  │        36.49 / 36.77 ±0.21 / 37.03 ms │        36.46 / 36.76 ±0.29 / 37.22 ms │     no change │
│ QQuery 9  │        52.55 / 53.63 ±0.90 / 55.12 ms │        53.94 / 54.83 ±1.03 / 56.80 ms │     no change │
│ QQuery 10 │        61.98 / 62.15 ±0.19 / 62.49 ms │        61.40 / 62.20 ±0.56 / 63.13 ms │     no change │
│ QQuery 11 │     293.06 / 294.59 ±1.47 / 297.03 ms │     291.31 / 294.19 ±1.92 / 297.29 ms │     no change │
│ QQuery 12 │        29.23 / 29.50 ±0.19 / 29.79 ms │        28.96 / 29.42 ±0.29 / 29.82 ms │     no change │
│ QQuery 13 │     117.84 / 118.62 ±0.52 / 119.20 ms │     117.83 / 118.84 ±0.57 / 119.53 ms │     no change │
│ QQuery 14 │     396.28 / 401.13 ±2.98 / 404.27 ms │     394.55 / 400.31 ±4.44 / 405.79 ms │     no change │
│ QQuery 15 │        56.78 / 60.92 ±5.78 / 71.93 ms │        55.99 / 56.75 ±0.84 / 58.31 ms │ +1.07x faster │
│ QQuery 16 │           6.67 / 6.89 ±0.19 / 7.22 ms │           6.49 / 6.69 ±0.15 / 6.89 ms │     no change │
│ QQuery 17 │        79.23 / 80.39 ±1.35 / 82.98 ms │        78.13 / 79.93 ±1.46 / 82.60 ms │     no change │
│ QQuery 18 │     104.93 / 106.64 ±1.18 / 108.26 ms │     104.74 / 105.26 ±0.49 / 106.01 ms │     no change │
│ QQuery 19 │        41.28 / 41.89 ±0.33 / 42.23 ms │        42.46 / 42.66 ±0.24 / 43.08 ms │     no change │
│ QQuery 20 │        36.04 / 36.47 ±0.63 / 37.71 ms │        35.81 / 36.59 ±0.67 / 37.83 ms │     no change │
│ QQuery 21 │        17.34 / 17.56 ±0.19 / 17.90 ms │        17.51 / 17.66 ±0.14 / 17.90 ms │     no change │
│ QQuery 22 │        64.50 / 65.99 ±0.76 / 66.60 ms │        65.25 / 65.83 ±0.39 / 66.45 ms │     no change │
│ QQuery 23 │     321.07 / 323.48 ±1.91 / 325.25 ms │     318.59 / 322.27 ±3.71 / 329.28 ms │     no change │
│ QQuery 24 │     199.28 / 202.38 ±2.40 / 206.60 ms │     199.22 / 202.57 ±2.00 / 205.43 ms │     no change │
│ QQuery 25 │     107.34 / 108.59 ±1.16 / 110.74 ms │     108.21 / 112.31 ±6.06 / 124.35 ms │     no change │
│ QQuery 26 │        48.47 / 48.86 ±0.38 / 49.55 ms │        48.72 / 49.36 ±0.63 / 50.43 ms │     no change │
│ QQuery 27 │           6.36 / 6.70 ±0.20 / 6.95 ms │           6.28 / 6.41 ±0.11 / 6.55 ms │     no change │
│ QQuery 28 │        56.51 / 59.59 ±2.23 / 61.76 ms │        58.45 / 61.00 ±1.29 / 62.00 ms │     no change │
│ QQuery 29 │        95.18 / 96.88 ±1.45 / 98.78 ms │      95.62 / 100.13 ±6.73 / 113.52 ms │     no change │
│ QQuery 30 │        32.99 / 33.43 ±0.23 / 33.62 ms │        33.33 / 33.72 ±0.32 / 34.23 ms │     no change │
│ QQuery 31 │     109.78 / 112.05 ±2.78 / 117.21 ms │     110.59 / 113.49 ±3.51 / 120.38 ms │     no change │
│ QQuery 32 │        20.87 / 21.11 ±0.18 / 21.33 ms │        20.91 / 21.05 ±0.14 / 21.30 ms │     no change │
│ QQuery 33 │        37.47 / 37.84 ±0.30 / 38.37 ms │        37.69 / 38.61 ±0.81 / 40.02 ms │     no change │
│ QQuery 34 │        10.38 / 10.61 ±0.24 / 11.08 ms │        10.34 / 10.74 ±0.28 / 11.04 ms │     no change │
│ QQuery 35 │        73.65 / 74.53 ±0.94 / 76.30 ms │        74.65 / 75.38 ±0.81 / 76.85 ms │     no change │
│ QQuery 36 │           6.16 / 6.32 ±0.14 / 6.54 ms │           5.85 / 6.05 ±0.18 / 6.38 ms │     no change │
│ QQuery 37 │           7.08 / 7.26 ±0.16 / 7.53 ms │           7.11 / 7.35 ±0.16 / 7.59 ms │     no change │
│ QQuery 38 │        63.21 / 64.13 ±0.93 / 65.72 ms │        63.88 / 65.24 ±1.95 / 69.00 ms │     no change │
│ QQuery 39 │        75.03 / 76.01 ±0.71 / 77.16 ms │        75.95 / 77.00 ±1.28 / 79.46 ms │     no change │
│ QQuery 40 │        24.83 / 25.02 ±0.19 / 25.32 ms │        24.21 / 24.75 ±0.36 / 25.09 ms │     no change │
│ QQuery 41 │        11.86 / 12.08 ±0.13 / 12.24 ms │        11.90 / 12.12 ±0.18 / 12.44 ms │     no change │
│ QQuery 42 │        24.10 / 24.73 ±0.45 / 25.48 ms │        24.09 / 24.66 ±0.60 / 25.64 ms │     no change │
│ QQuery 43 │           5.37 / 5.46 ±0.13 / 5.73 ms │           5.31 / 5.44 ±0.15 / 5.72 ms │     no change │
│ QQuery 44 │           9.55 / 9.68 ±0.09 / 9.81 ms │         9.62 / 11.31 ±3.07 / 17.44 ms │  1.17x slower │
│ QQuery 45 │        40.12 / 40.89 ±0.69 / 42.07 ms │        40.12 / 41.02 ±0.62 / 41.71 ms │     no change │
│ QQuery 46 │        12.20 / 12.57 ±0.35 / 13.18 ms │        12.49 / 12.90 ±0.34 / 13.34 ms │     no change │
│ QQuery 47 │     232.51 / 238.16 ±4.52 / 244.48 ms │     230.78 / 241.81 ±7.76 / 253.73 ms │     no change │
│ QQuery 48 │        95.86 / 96.57 ±0.45 / 97.27 ms │        95.84 / 96.40 ±0.35 / 96.83 ms │     no change │
│ QQuery 49 │        71.10 / 71.57 ±0.47 / 72.41 ms │        70.79 / 72.12 ±1.85 / 75.75 ms │     no change │
│ QQuery 50 │        58.59 / 59.87 ±1.91 / 63.64 ms │        58.08 / 59.76 ±2.15 / 63.94 ms │     no change │
│ QQuery 51 │       93.28 / 95.87 ±3.85 / 103.29 ms │       92.52 / 97.62 ±6.34 / 109.90 ms │     no change │
│ QQuery 52 │        24.41 / 24.78 ±0.50 / 25.75 ms │        26.13 / 26.63 ±0.51 / 27.33 ms │  1.07x slower │
│ QQuery 53 │        29.49 / 29.61 ±0.13 / 29.83 ms │        29.38 / 30.81 ±0.74 / 31.39 ms │     no change │
│ QQuery 54 │        54.60 / 55.14 ±0.64 / 56.34 ms │        54.51 / 54.87 ±0.42 / 55.69 ms │     no change │
│ QQuery 55 │        23.75 / 24.06 ±0.25 / 24.37 ms │        23.79 / 24.11 ±0.31 / 24.51 ms │     no change │
│ QQuery 56 │        39.10 / 39.38 ±0.20 / 39.63 ms │        38.43 / 39.09 ±0.43 / 39.59 ms │     no change │
│ QQuery 57 │     169.17 / 172.29 ±3.03 / 177.89 ms │     169.69 / 172.96 ±2.74 / 177.19 ms │     no change │
│ QQuery 58 │     109.97 / 111.33 ±1.00 / 112.68 ms │     109.47 / 111.63 ±1.75 / 114.82 ms │     no change │
│ QQuery 59 │     116.72 / 117.40 ±0.77 / 118.79 ms │     116.51 / 117.22 ±0.42 / 117.82 ms │     no change │
│ QQuery 60 │        39.31 / 40.36 ±1.18 / 42.58 ms │        39.19 / 39.50 ±0.23 / 39.77 ms │     no change │
│ QQuery 61 │        11.87 / 11.98 ±0.09 / 12.08 ms │        11.97 / 12.10 ±0.11 / 12.28 ms │     no change │
│ QQuery 62 │        44.68 / 45.66 ±1.18 / 47.97 ms │        45.24 / 45.78 ±0.83 / 47.42 ms │     no change │
│ QQuery 63 │        29.65 / 30.09 ±0.31 / 30.40 ms │        29.81 / 30.28 ±0.48 / 31.11 ms │     no change │
│ QQuery 64 │     365.35 / 368.33 ±2.01 / 371.43 ms │     368.69 / 370.73 ±1.78 / 372.88 ms │     no change │
│ QQuery 65 │     130.61 / 135.18 ±2.57 / 138.58 ms │     132.23 / 137.03 ±3.16 / 141.38 ms │     no change │
│ QQuery 66 │        77.36 / 77.70 ±0.44 / 78.55 ms │        77.88 / 79.88 ±2.80 / 85.39 ms │     no change │
│ QQuery 67 │     258.25 / 265.77 ±6.46 / 274.59 ms │     260.60 / 262.88 ±3.61 / 270.07 ms │     no change │
│ QQuery 68 │        12.24 / 12.33 ±0.09 / 12.50 ms │        12.20 / 12.47 ±0.20 / 12.69 ms │     no change │
│ QQuery 69 │        57.20 / 60.08 ±5.39 / 70.86 ms │        57.48 / 60.64 ±4.84 / 70.22 ms │     no change │
│ QQuery 70 │     104.55 / 106.54 ±1.70 / 109.40 ms │     105.89 / 108.07 ±1.94 / 110.45 ms │     no change │
│ QQuery 71 │        35.53 / 36.00 ±0.43 / 36.66 ms │        35.43 / 35.97 ±0.30 / 36.28 ms │     no change │
│ QQuery 72 │ 1830.92 / 1901.74 ±46.05 / 1953.53 ms │ 1814.12 / 1839.72 ±19.92 / 1865.70 ms │     no change │
│ QQuery 73 │        10.18 / 10.53 ±0.31 / 11.07 ms │        10.17 / 11.10 ±0.68 / 11.88 ms │  1.05x slower │
│ QQuery 74 │     169.50 / 173.56 ±2.66 / 177.00 ms │     174.00 / 176.73 ±2.07 / 180.24 ms │     no change │
│ QQuery 75 │     142.65 / 143.75 ±1.11 / 145.85 ms │     143.38 / 145.41 ±2.01 / 148.83 ms │     no change │
│ QQuery 76 │        35.39 / 37.86 ±3.56 / 44.89 ms │        34.16 / 34.79 ±0.46 / 35.50 ms │ +1.09x faster │
│ QQuery 77 │        60.90 / 61.40 ±0.30 / 61.64 ms │        62.20 / 62.64 ±0.38 / 63.08 ms │     no change │
│ QQuery 78 │     163.97 / 169.22 ±5.40 / 177.60 ms │     164.63 / 169.51 ±5.09 / 178.87 ms │     no change │
│ QQuery 79 │        66.77 / 67.74 ±0.63 / 68.62 ms │        67.05 / 67.80 ±0.54 / 68.73 ms │     no change │
│ QQuery 80 │      97.80 / 100.66 ±3.62 / 107.58 ms │        96.14 / 97.15 ±0.95 / 98.93 ms │     no change │
│ QQuery 81 │        26.62 / 26.70 ±0.08 / 26.84 ms │        26.37 / 28.79 ±3.97 / 36.71 ms │  1.08x slower │
│ QQuery 82 │        16.91 / 17.20 ±0.20 / 17.54 ms │        17.23 / 17.28 ±0.05 / 17.37 ms │     no change │
│ QQuery 83 │        34.27 / 34.78 ±0.42 / 35.52 ms │        34.29 / 34.51 ±0.22 / 34.91 ms │     no change │
│ QQuery 84 │        29.97 / 30.17 ±0.16 / 30.42 ms │        30.31 / 31.25 ±1.44 / 34.13 ms │     no change │
│ QQuery 85 │     104.39 / 108.66 ±6.79 / 122.11 ms │     103.66 / 104.61 ±0.57 / 105.27 ms │     no change │
│ QQuery 86 │        26.26 / 26.76 ±0.26 / 27.03 ms │        26.00 / 29.13 ±5.78 / 40.68 ms │  1.09x slower │
│ QQuery 87 │        63.91 / 65.05 ±0.95 / 66.52 ms │        64.30 / 65.11 ±0.79 / 66.46 ms │     no change │
│ QQuery 88 │        61.73 / 62.18 ±0.40 / 62.85 ms │        62.09 / 62.83 ±0.45 / 63.30 ms │     no change │
│ QQuery 89 │        36.05 / 36.45 ±0.41 / 37.21 ms │        35.63 / 35.81 ±0.18 / 36.14 ms │     no change │
│ QQuery 90 │        17.13 / 17.30 ±0.14 / 17.50 ms │        17.14 / 19.32 ±4.05 / 27.41 ms │  1.12x slower │
│ QQuery 91 │        44.74 / 45.63 ±0.98 / 47.47 ms │        45.93 / 46.47 ±0.48 / 47.35 ms │     no change │
│ QQuery 92 │        29.98 / 30.54 ±0.52 / 31.38 ms │        30.78 / 31.91 ±1.42 / 34.68 ms │     no change │
│ QQuery 93 │        49.44 / 49.88 ±0.44 / 50.74 ms │        49.18 / 50.73 ±0.95 / 51.68 ms │     no change │
│ QQuery 94 │        39.13 / 40.68 ±1.79 / 44.16 ms │        38.98 / 39.57 ±0.33 / 39.89 ms │     no change │
│ QQuery 95 │        80.22 / 81.72 ±1.75 / 84.67 ms │        79.99 / 82.39 ±2.70 / 87.42 ms │     no change │
│ QQuery 96 │        24.21 / 24.35 ±0.08 / 24.44 ms │        24.09 / 24.61 ±0.85 / 26.31 ms │     no change │
│ QQuery 97 │        51.17 / 52.37 ±1.33 / 54.95 ms │        51.36 / 52.56 ±1.30 / 55.04 ms │     no change │
│ QQuery 98 │        43.37 / 45.01 ±2.90 / 50.80 ms │        43.74 / 44.00 ±0.40 / 44.80 ms │     no change │
│ QQuery 99 │        65.90 / 66.19 ±0.31 / 66.70 ms │        66.42 / 66.70 ±0.20 / 66.91 ms │     no change │
└───────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary                                ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (HEAD)                                │ 9445.56ms │
│ Total Time (multi-column-null-aware-anti-join)   │ 9412.67ms │
│ Average Time (HEAD)                              │   95.41ms │
│ Average Time (multi-column-null-aware-anti-join) │   95.08ms │
│ Queries Faster                                   │         2 │
│ Queries Slower                                   │         6 │
│ Queries with No Change                           │        91 │
│ Queries with Failure                             │         0 │
└──────────────────────────────────────────────────┴───────────┘

Resource Usage

tpcds — base (merge-base)

Metric Value
Wall time 50.0s
Peak memory 1.9 GiB
Avg memory 1.4 GiB
CPU user 207.3s
CPU sys 5.2s
Peak spill 0 B

tpcds — branch

Metric Value
Wall time 50.0s
Peak memory 2.1 GiB
Avg memory 1.4 GiB
CPU user 206.7s
CPU sys 5.5s
Peak spill 0 B

File an issue against this benchmark runner

@viirya
viirya requested a review from comphead September 30, 2026 00:57
@viirya

viirya commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

@sunchao Can you take a look? Thanks!

@comphead

comphead commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

@viirya checking this again

@comphead comphead 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 @viirya. I re-read the changes since 8eb53c4 and the two upstream merges. The -0.0, non-hashable key, all-literal tuple and per-chunk comparison fixes look right to me. I also checked the merge resolution against the new MarkJoin code in decorrelate_predicate_subquery.rs, and every expected row in null_aware_multi_column.slt against three-valued logic. I found nothing wrong there.

Verdict: the approach and the hash join side look good to me. I would hold the merge for the two inline points, the tuple nullability in Expr::nullable and the conflict with main in type_coercion.rs. The points below are smaller. All of them come from reading the code, not from running the branch.

Tests

  • Nothing covers IS [NOT] NULL over a tuple IN, and every tuple case uses nullable subquery columns. That is why the nullability issue inline on in_subquery_tuple_values stays hidden.
  • subquery_output_expr has an arm for a subquery whose top is not a Projection (DISTINCT, GROUP BY, ORDER BY ... LIMIT). Only a plain SELECT x, y is tested.
  • Tuple elements are only INT, BIGINT and DOUBLE. A Utf8 element, the most common shape, would also run take plus apply_cmp on a non-primitive array in retain_value_mismatch_free.
  • The mark join is only tested with an equality correlation. A LeftMark tuple with i.k < o.k would cover the join filter branch there.
  • No fuzz test covers null-aware joins. The NOT EXISTS formulations in the Q10 to Q12 canaries are a ready oracle for a randomized comparison across batch sizes and NULL densities.

Docs

  • docs/source/user-guide/sql/subqueries.md still says non-scalar subqueries "can only return a single column" (twice) and documents tuple IN only in its list form. The list form uses struct equality, so per that page (1, NULL) IN ((1, NULL)) is true. Here (1, NULL) IN (SELECT 1, NULL) is now NULL under SQL row comparison. That difference deserves a sentence there.

Follow-ups

  • I could only find the two follow-ups agreed in the earlier thread (NOT NULL elements as scope keys, and building on the subquery side) in the doc comment on HashJoinExec::null_aware_value_keys. Could you file issues for them and link those from the comment? The outer side always being the build side and the quadratic NULL pairing are the user-visible costs. Q10 to Q12 numbers at two NAJ_ROWS values would also make the follow-ups measurable.

Nit

  • Placeholder inference builds the per-element column with Column::new_unqualified(field.name()). That is ambiguous when two subquery columns share a name under different qualifiers (SELECT t1.id, t2.id ...). Column::from(subquery_schema.qualified_field(i)) avoids it, as subquery_output_expr already does.

///
/// Errors when the number of tuple elements does not match the number of
/// subquery columns.
pub fn in_subquery_tuple_values<'a>(

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.

This helper decides when an InSubquery is a tuple, but Expr::nullable (expr_schema.rs, InSubquery arm) still assumes one value. It returns expr.nullable() | <first subquery column>.nullable(). For a tuple, expr is a struct(..) call, and struct always reports a non-nullable field (return_field_from_args). So the elements never count, and only the first subquery column does. (a, b) IN (SELECT 1, 5) is typed NOT NULL, although it is UNKNOWN for (NULL, 5).

simplify_expressions runs before decorrelation and folds IS NULL and IS UNKNOWN to false, and IS NOT NULL to true, from that flag. By my reading this gives a wrong result. I did not run it:

CREATE TABLE o(a INT, b INT) AS VALUES (NULL, 5), (1, 5);
SELECT a, ((a, b) IN (SELECT 1, 5)) IS NULL AS unk FROM o;
-- expected: (NULL, true) and (1, false)
-- by my reading: false for both rows

The scalar form is exact because a.nullable | x.nullable covers both sides. For a tuple the arm could return true when any element or any subquery column is nullable. An SLT case with IS NULL over a tuple IN and a NOT NULL first subquery column would pin it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed: ((a, b) IN (SELECT 1, 5)) IS NULL folded to false for (NULL, 5). Fixed in ab3120c. The InSubquery arm of Expr::nullable now treats a tuple IN as nullable when any element or any subquery column is nullable. null_aware_multi_column.slt covers IS NULL and IS NOT NULL over a tuple IN whose subquery columns are both NOT NULL.

/// Each tuple element and the subquery column at the same position are cast
/// to their common comparison type, like the single-column form does for its
/// one pair.
fn coerce_multi_column_in_subquery(

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.

This PR now conflicts with main in this file. #25094 changed the scalar branch of this arm to comparison_coercion_with_session_timezone(.., self.session_time_zone). This helper is a free function, so a resolution that only patches the scalar branch still compiles and leaves tuple elements on the timezone-unaware comparison_coercion. It needs the session time zone passed in as well. A tuple with a naive and a tz-aware timestamp element, with the session time zone set, would catch a miss.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Merged main and passed the session time zone into coerce_multi_column_in_subquery, so the scalar branch and every tuple element both use comparison_coercion_with_session_timezone. The new case in null_aware_multi_column.slt compares a naive and a time-zone-aware timestamp element under SET TIME ZONE = '+08'. It fails if the tuple path drops the session time zone, because the naive value is then read in the aware value's own zone and matches.

@viirya

viirya commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Thanks @comphead. ab3120c addresses the review:

Tests

  • null_aware_multi_column.slt now covers:
    • IS [NOT] NULL over a tuple IN
    • subqueries whose top is DISTINCT, GROUP BY or ORDER BY ... LIMIT
    • Utf8 elements
    • a LeftMark tuple with an i.k < o.k correlation
    • the session time zone case
    • placeholders over two same-named subquery columns
  • Added fuzz_cases/null_aware_join_fuzz.rs. It compares scalar and multi-column NOT IN / IN, in filter, OR and value contexts, with and without equality and non-equality correlations, against NOT EXISTS / EXISTS formulations of the same three-valued logic. It sweeps random tables with NULLs on both sides across batch sizes and partition counts.

Docs

  • subqueries.md now documents the tuple form, allows one subquery column per tuple element in both places that said "a single column", and explains that (1, NULL) IN (SELECT 1, NULL) is NULL under row comparison while the list form is true. The examples are pinned in the SLT.

Follow-ups

query NAJ_ROWS=10000 NAJ_ROWS=20000
Q10, no NULLs 0.32 ms 0.38 ms
Q11, 1% NULLs in the subquery 3.64 ms 8.07 ms
Q12, 1% NULLs in the outer side 0.56 ms 0.76 ms

Nit

  • Placeholder inference now uses Column::from(subquery_schema.qualified_field(i)).

@comphead

comphead commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Thanks @viirya
@neilconway please let us know your feedback as you requested changes

@viirya

viirya commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Thank you @comphead

@neilconway

Copy link
Copy Markdown
Contributor

Won't get a chance to do a careful review right now, but the points I raised before have been addressed -- no objections to landing this from me!

@viirya

viirya commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Won't get a chance to do a careful review right now, but the points I raised before have been addressed -- no objections to landing this from me!

Thank you @neilconway !

Could you remove the requested changes request because seems it will block merging? Thanks!

@neilconway neilconway 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.

I had some old pending comments, not sure if they still apply :) Feel free to ignore if so.

let is_struct = matches!(*expr, Expr::ScalarFunction(ref func) if func.func.name() == "struct");

if is_struct {
// For multi-column IN, we don't need type coercion at this level

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.

Can you elaborate on this? It isn't clear to me why the type coercion requirements for multi-column IN would be different than for a single-column IN.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This one is out of date: the multi-column form now coerces each tuple element against its subquery column, the same way the single-column form coerces its one pair (coerce_multi_column_in_subquery, which also applies the session time zone).

if subquery.subquery.subquery.schema().fields().len() > 1 {
// InSubquery should only return one column UNLESS the left expression is a struct
// (multi-column IN like: (a, b) NOT IN (SELECT x, y FROM ...))
let is_struct = matches!(*subquery.expr, Expr::ScalarFunction(ref func) if func.func.name() == "struct");

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.

Matching based on the function name here seems a bit fragile, which is unfortunate because we do it repeatedly in this PR. Is there a way to make the distinction more principled / manifest in the type system?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. The name check now lives in one place, in_subquery_tuple_values, which every caller goes through (SQL planning, invariants, type coercion, nullability, placeholder inference and decorrelation). It also recognizes the struct literal that simplify_expressions folds an all-literal tuple into. Making the tuple explicit in the type system, for example as a dedicated field on InSubquery, would be cleaner but changes a public expression type.

@comphead comphead 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 @viirya

@viirya
viirya added this pull request to the merge queue Oct 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 7, 2026
@viirya
viirya added this pull request to the merge queue Oct 7, 2026
Merged via the queue into apache:main with commit 532cd03 Oct 7, 2026
43 checks passed
@viirya
viirya deleted the multi-column-null-aware-anti-join branch October 7, 2026 02:08
@viirya

viirya commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Thanks all for reviewing this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate auto detected api change Auto detected API change core Core DataFusion crate documentation Improvements or additions to documentation logical-expr Logical plan and expressions optimizer Optimizer rules physical-plan Changes to the physical-plan crate proto Related to proto crate sql SQL Planner sqllogictest SQL Logic Tests (.slt) v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support multi-column (tuple) NOT IN subqueries with null-aware semantics

6 participants