Skip to content

perf: use one null-aware mark join for hashable IN subqueries in projections - #25338

Merged
adriangb merged 3 commits into
apache:mainfrom
pydantic:in-exists-subquery-projection
Sep 29, 2026
Merged

adriangb merged 3 commits into
apache:mainfrom
pydantic:in-exists-subquery-projection

Conversation

@adriangb

@adriangb adriangb commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

This PR supersedes #21363 by @crm26. It reuses the single mark join approach from that PR, and @crm26 is a co-author of the main commit.

It also builds on #24972, which added the decorrelation of IN subqueries in a projection.

Rationale for this change

An IN or NOT IN subquery in a SELECT list gives correct results today, but the plan is quadratic. The optimizer builds three mark joins for each subquery. Two of them have no join predicate, so they run as nested loop joins over all outer rows and all inner rows. COALESCE duplicates the expression, so that shape gets six mark joins.

This PR keeps one hash mark join for each subquery. The join is null-aware when a key can be NULL. A non-equality correlation (Q6 below) stays a residual filter of that join, which the null-aware hash join applies since #25339.

Shape Query main this PR Result rows identical
Q1 Bare IN in the SELECT list 12.893 s 0.005 s yes
Q2 COALESCE((x IN (...))::boolean, false) 36.430 s 0.014 s yes
Q3 Correlated IN with an equality predicate 0.278 s 0.008 s yes
Q4 Two IN subqueries in separate columns 20.904 s 0.011 s yes
Q5 Correlated EXISTS 0.005 s 0.003 s yes
Q6 Correlated IN with a non-equality predicate 2.489 s 0.018 s yes

The table is one run of the script below after the rebase onto main at 39ca2c74a9, datafusion-cli built with --profile release-nonlto and the default target_partitions. Q6 in 3 more interleaved runs: main 2.57 s to 3.18 s, this PR 0.022 s to 0.058 s.

These shapes are the projection_subquery benchmark suite from #25346.

Reproduction with datafusion-cli: script, plans and timings for each shape

Run the script with datafusion-cli -f mre.sql. It creates two tables with 200000 rows each, then for each shape it prints EXPLAIN and runs the query. datafusion-cli prints the elapsed time after each statement. The result rows of every query are identical between the two builds.

The plans and times for Q1 to Q5 below are from the first version of this PR, with main at 22651d2 and target_partitions = 4. The plans of this PR for those shapes did not change with the rebase. Q6 was captured again after the rebase.

SET datafusion.execution.target_partitions = 4;
CREATE TABLE outer_t AS SELECT CAST(v AS INT) AS id, CAST(v % 1000 AS INT) AS z FROM (SELECT unnest(generate_series(1, 200000)) AS v);
CREATE TABLE inner_t AS SELECT CASE WHEN v % 97 = 0 THEN NULL ELSE CAST(v * 2 AS INT) END AS id, CAST(v % 1000 AS INT) AS z FROM (SELECT unnest(generate_series(1, 200000)) AS v);
SELECT 'Q1' AS shape;
EXPLAIN SELECT id, id IN (SELECT id FROM inner_t) AS m FROM outer_t;
SELECT count(*) FILTER (WHERE m), count(*) FILTER (WHERE m IS NULL) FROM (SELECT id, id IN (SELECT id FROM inner_t) AS m FROM outer_t);
SELECT 'Q2' AS shape;
EXPLAIN SELECT id, COALESCE((id IN (SELECT id FROM inner_t))::boolean, false) AS m FROM outer_t;
SELECT count(*) FILTER (WHERE m) FROM (SELECT id, COALESCE((id IN (SELECT id FROM inner_t))::boolean, false) AS m FROM outer_t);
SELECT 'Q3' AS shape;
EXPLAIN SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z = o.z) AS m FROM outer_t o;
SELECT count(*) FILTER (WHERE m), count(*) FILTER (WHERE m IS NULL) FROM (SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z = o.z) AS m FROM outer_t o);
SELECT 'Q4' AS shape;
EXPLAIN SELECT id IN (SELECT id FROM inner_t) AS a, id IN (SELECT id FROM inner_t WHERE z < 500) AS b FROM outer_t;
SELECT count(*) FILTER (WHERE a), count(*) FILTER (WHERE b) FROM (SELECT id IN (SELECT id FROM inner_t) AS a, id IN (SELECT id FROM inner_t WHERE z < 500) AS b FROM outer_t);
SELECT 'Q5' AS shape;
EXPLAIN SELECT id, EXISTS (SELECT 1 FROM inner_t i WHERE i.id = o.id) AS e FROM outer_t o;
SELECT count(*) FILTER (WHERE e) FROM (SELECT id, EXISTS (SELECT 1 FROM inner_t i WHERE i.id = o.id) AS e FROM outer_t o);
SELECT 'Q6' AS shape;
EXPLAIN SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z < o.z) AS m FROM outer_t o;
SELECT count(*) FILTER (WHERE m), count(*) FILTER (WHERE m IS NULL) FROM (SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z < o.z) AS m FROM outer_t o);
Q1: Bare `IN` in the SELECT list, plans on main and on this PR

main, query time 190.730 s

logical_plan
Projection: outer_t.id, __correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR outer_t.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS m
  LeftMark Join:
    LeftMark Join:
      LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
        TableScan: outer_t projection=[id]
        SubqueryAlias: __correlated_sq_1
          TableScan: inner_t projection=[id]
      SubqueryAlias: __correlated_sq_2
        Filter: inner_t.id IS NULL
          TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_3
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL as m]
  NestedLoopJoinExec: join_type=LeftMark
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark
        CoalescePartitionsExec
          FilterExec: id@0 IS NULL
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
          CoalescePartitionsExec
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

this PR, query time 0.034 s

logical_plan
Projection: outer_t.id, __correlated_sq_1.mark AS m
  LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
    TableScan: outer_t projection=[id]
    SubqueryAlias: __correlated_sq_1
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

Q2: `COALESCE((x IN (...))::boolean, false)`, plans on main and on this PR

main, query time 351.579 s

logical_plan
Projection: outer_t.id, CAST(__correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR outer_t.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS Boolean) IS NOT NULL AND CAST(__correlated_sq_4.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_5.mark OR outer_t.id IS NULL AND __correlated_sq_6.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_4.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS Boolean) AS m
  LeftMark Join:
    LeftMark Join:
      LeftMark Join: outer_t.id = __correlated_sq_4.id null_aware
        LeftMark Join:
          LeftMark Join:
            LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
              TableScan: outer_t projection=[id]
              SubqueryAlias: __correlated_sq_1
                TableScan: inner_t projection=[id]
            SubqueryAlias: __correlated_sq_2
              Filter: inner_t.id IS NULL
                TableScan: inner_t projection=[id]
          SubqueryAlias: __correlated_sq_3
            TableScan: inner_t projection=[id]
        SubqueryAlias: __correlated_sq_4
          TableScan: inner_t projection=[id]
      SubqueryAlias: __correlated_sq_5
        Filter: inner_t.id IS NULL
          TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_6
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL IS NOT NULL AND (mark@4 IS NOT DISTINCT FROM true OR (mark@5 OR id@0 IS NULL AND mark@6) IS NOT DISTINCT FROM true AND mark@4 IS DISTINCT FROM true AND NULL) as m]
  NestedLoopJoinExec: join_type=LeftMark
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark
        CoalescePartitionsExec
          FilterExec: id@0 IS NULL
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
          CoalescePartitionsExec
            NestedLoopJoinExec: join_type=LeftMark
              CoalescePartitionsExec
                NestedLoopJoinExec: join_type=RightMark
                  CoalescePartitionsExec
                    FilterExec: id@0 IS NULL
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
                    CoalescePartitionsExec
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
              DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

this PR, query time 0.054 s

logical_plan
Projection: outer_t.id, CAST(__correlated_sq_1.mark AS Boolean) IS NOT NULL AND CAST(__correlated_sq_2.mark AS Boolean) AS m
  LeftMark Join: outer_t.id = __correlated_sq_2.id null_aware
    LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
      TableScan: outer_t projection=[id]
      SubqueryAlias: __correlated_sq_1
        TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_2
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT NULL AND mark@2 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
    CoalescePartitionsExec
      HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
        CoalescePartitionsExec
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

Q3: Correlated `IN` with an equality predicate, plans on main and on this PR

main, query time 1.372 s

logical_plan
Projection: o.id, __correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR o.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS m
  LeftMark Join: o.z = __correlated_sq_3.z
    LeftMark Join: o.z = __correlated_sq_2.z
      LeftMark Join: o.id = __correlated_sq_1.id, o.z = __correlated_sq_1.z null_aware
        SubqueryAlias: o
          TableScan: outer_t projection=[id, z]
        SubqueryAlias: __correlated_sq_1
          SubqueryAlias: i
            TableScan: inner_t projection=[id, z]
      SubqueryAlias: __correlated_sq_2
        SubqueryAlias: i
          Projection: inner_t.z
            Filter: inner_t.id IS NULL
              TableScan: inner_t projection=[id, z]
    SubqueryAlias: __correlated_sq_3
      SubqueryAlias: i
        TableScan: inner_t projection=[z]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL as m]
  HashJoinExec: mode=CollectLeft, join_type=RightMark, on=[(z@0, z@1)], projection=[id@0, mark@2, mark@3, mark@4]
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    HashJoinExec: mode=CollectLeft, join_type=RightMark, on=[(z@0, z@1)]
      CoalescePartitionsExec
        FilterExec: id@0 IS NULL, projection=[z@1]
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
      HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0), (z@1, z@1)], null_aware
        CoalescePartitionsExec
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

this PR, query time 0.091 s

logical_plan
Projection: o.id, __correlated_sq_1.mark AS m
  LeftMark Join: o.id = __correlated_sq_1.id, o.z = __correlated_sq_1.z null_aware
    SubqueryAlias: o
      TableScan: outer_t projection=[id, z]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0), (z@1, z@1)], projection=[id@0, mark@2], null_aware
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

Q4: Two `IN` subqueries in separate columns, plans on main and on this PR

main, query time 296.676 s

logical_plan
Projection: __correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR outer_t.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS a, __correlated_sq_4.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_5.mark OR outer_t.id IS NULL AND __correlated_sq_6.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_4.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS b
  LeftMark Join:
    LeftMark Join:
      LeftMark Join: outer_t.id = __correlated_sq_4.id null_aware
        LeftMark Join:
          LeftMark Join:
            LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
              TableScan: outer_t projection=[id]
              SubqueryAlias: __correlated_sq_1
                TableScan: inner_t projection=[id]
            SubqueryAlias: __correlated_sq_2
              Filter: inner_t.id IS NULL
                TableScan: inner_t projection=[id]
          SubqueryAlias: __correlated_sq_3
            TableScan: inner_t projection=[id]
        SubqueryAlias: __correlated_sq_4
          Projection: inner_t.id
            Filter: inner_t.z < Int32(500)
              TableScan: inner_t projection=[id, z]
      SubqueryAlias: __correlated_sq_5
        Projection: inner_t.id
          Filter: inner_t.z < Int32(500) AND inner_t.id IS NULL
            TableScan: inner_t projection=[id, z]
    SubqueryAlias: __correlated_sq_6
      Projection: inner_t.id
        Filter: inner_t.z < Int32(500)
          TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL as a, mark@4 IS NOT DISTINCT FROM true OR (mark@5 OR id@0 IS NULL AND mark@6) IS NOT DISTINCT FROM true AND mark@4 IS DISTINCT FROM true AND NULL as b]
  NestedLoopJoinExec: join_type=LeftMark
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark
        CoalescePartitionsExec
          FilterExec: z@1 < 500 AND id@0 IS NULL, projection=[id@0]
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
          CoalescePartitionsExec
            NestedLoopJoinExec: join_type=LeftMark
              CoalescePartitionsExec
                NestedLoopJoinExec: join_type=RightMark
                  CoalescePartitionsExec
                    FilterExec: id@0 IS NULL
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
                    CoalescePartitionsExec
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
              DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
          FilterExec: z@1 < 500, projection=[id@0]
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    FilterExec: z@1 < 500, projection=[id@0]
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

this PR, query time 0.063 s

logical_plan
Projection: __correlated_sq_1.mark AS a, __correlated_sq_2.mark AS b
  LeftMark Join: outer_t.id = __correlated_sq_2.id null_aware
    LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
      TableScan: outer_t projection=[id]
      SubqueryAlias: __correlated_sq_1
        TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_2
      Projection: inner_t.id
        Filter: inner_t.z < Int32(500)
          TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[mark@0 as a, mark@1 as b]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], projection=[mark@1, mark@2], null_aware
    CoalescePartitionsExec
      HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
        CoalescePartitionsExec
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    FilterExec: z@1 < 500, projection=[id@0]
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

Q5: Correlated `EXISTS`, plans on main and on this PR

main, query time 0.021 s

logical_plan
Projection: o.id, __correlated_sq_1.mark AS e
  LeftMark Join: o.id = __correlated_sq_1.id
    SubqueryAlias: o
      TableScan: outer_t projection=[id]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as e]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)]
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

this PR, query time 0.021 s

logical_plan
Projection: o.id, __correlated_sq_1.mark AS e
  LeftMark Join: o.id = __correlated_sq_1.id
    SubqueryAlias: o
      TableScan: outer_t projection=[id]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as e]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)]
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

Q6: Correlated `IN` with a non-equality predicate, plans on main and on this PR

main (39ca2c74a9), query time 2.489 s

logical_plan
Projection: o.id, CASE WHEN __correlated_sq_1.mark THEN Boolean(true) WHEN __correlated_sq_2.mark OR o.id IS NULL AND __correlated_sq_3.mark THEN Boolean(NULL) ELSE Boolean(false) END AS m
  LeftMark Join:  Filter: __correlated_sq_3.z < o.z
    LeftMark Join:  Filter: __correlated_sq_2.z < o.z
      LeftMark Join: o.id = __correlated_sq_1.id Filter: __correlated_sq_1.z < o.z null_aware
        SubqueryAlias: o
          TableScan: outer_t projection=[id, z]
        SubqueryAlias: __correlated_sq_1
          SubqueryAlias: i
            TableScan: inner_t projection=[id, z]
      SubqueryAlias: __correlated_sq_2
        SubqueryAlias: i
          Projection: inner_t.z
            Filter: inner_t.id IS NULL
              TableScan: inner_t projection=[id, z]
    SubqueryAlias: __correlated_sq_3
      SubqueryAlias: i
        TableScan: inner_t projection=[z]
physical_plan
ProjectionExec: expr=[id@0 as id, CASE WHEN mark@1 THEN true WHEN mark@2 OR id@0 IS NULL AND mark@3 THEN NULL ELSE false END as m]
  NestedLoopJoinExec: join_type=LeftMark, filter=z@1 < z@0, projection=[id@0, mark@2, mark@3, mark@4]
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark, filter=z@1 < z@0
        CoalescePartitionsExec
          FilterExec: id@0 IS NULL, projection=[z@1]
            DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], filter=z@1 < z@0, null_aware
          CoalescePartitionsExec
            DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
          DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
    DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

this PR, query time 0.018 s

logical_plan
Projection: o.id, __correlated_sq_1.mark AS m
  LeftMark Join: o.id = __correlated_sq_1.id Filter: __correlated_sq_1.z < o.z null_aware
    SubqueryAlias: o
      TableScan: outer_t projection=[id, z]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], filter=z@1 < z@0, projection=[id@0, mark@2], null_aware
    CoalescePartitionsExec
      DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
    DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

What changes are included in this PR?

  1. build_join now reports whether the mark column of the LeftMark join is exact under three-valued logic. It is exact when no side of the IN predicate can be NULL in scope, or when the join is null-aware and the IN equality is its first key. A residual non-equality filter does not change this, because the null-aware hash join applies it when it decides whether a NULL makes the mark UNKNOWN (fix: correlated NOT IN with a non-equality correlation returns wrong results #25339). in_subquery_value_mark_join builds this join first. If the mark is exact, it returns the mark column alone, or NOT mark for NOT IN. The three join materialization stays for the other cases.

  2. The rule decides null-awareness from one question: can the IN value or the subquery output be NULL inside the scope of an outer row? Only then can IN be UNKNOWN.

    • The test uses the key expressions, not the columns they reference. A key such as NULLIF(id, 1) or TRY_CAST(s AS INT) can be NULL over a NOT NULL column.
    • PullUpCorrelatedExpr now records every correlated conjunct, before it drops the conjunct that repeats the IN predicate. A conjunct such as y = x, y > x or y IS NOT NULL is never TRUE for a NULL y, so it keeps a NULL y out of the scope. The conjuncts keep their outer references, so a subquery column with the same qualified name as an outer column is not mistaken for the outer value.
    • A correlation key that is NULL only empties the scope, so it never makes the join null-aware.
    • The recorded conjuncts are cleared when the pull up passes an outer join, a union or a grouping set, because such a node can put a NULL back into the column. With ROLLUP, the grand-total row is a NULL for every outer row.
  3. A NOT IN filter uses the same scope-aware nullability for its null-aware LeftAnti join. If the IN equality does not become the first key of that join, the rule gives up and the NOT IN becomes the mark joins, which give the correct result.

  4. The regression test for fix: scan subqueries when advancing the extracted-alias generator #24574 in projection_pushdown.slt now uses a correlated subquery with LIMIT 1. The subquery then still reaches ExtractLeafExpressions, and the alias generator still starts at 2 inside it. The earlier version of the test let the subquery be flattened, so it no longer exercised the scan inside a subquery path.

  5. New sqllogictest cases in subquery_projection.slt:

    • EXPLAIN guards: one hash mark join for each subquery, one null-aware mark join with a residual filter, a null-aware LeftAnti join with two keys, and with one key and a residual filter.
    • NULL semantics: IN and NOT IN with a NULL in the subquery, a NULL outer value, NOT inside CASE, a projection over an aggregate, the COALESCE and cast shape, and the residual filter shape.
    • Nullable key expressions over NOT NULL columns: a NULLIF value, a TRY_CAST value and a NULLIF subquery output, in a projection and in a WHERE clause.
    • A correlation that repeats the IN predicate, and a subquery column with the same qualified name as the outer value.
    • The same correlation below a ROLLUP, as a projected IN with an EXPLAIN guard and as a NOT IN filter.

    The expected results agree with DuckDB 1.5.2, and the earlier cases also with PostgreSQL.

  6. The projection_subquery benchmark docs no longer describe q07 as the nested-loop control.

DuckDB comparison

For an absolute reference, the same seven queries in datafusion-cli and in DuckDB 1.5.2, on the same two tables, release build, Apple M4 Pro. main is the merge-base 64871d9 and "this PR" is 3af87370c1. The later commits of this PR change the correlated and NOT IN filter paths, and, after the rebase onto #25339, the plan of q07. q07 is measured again below. The tables are the ones the suite builds, at its default of 30,000 rows each. Each number is the median of 7 runs. One round runs one session per engine, and the rounds interleave the three engines.

Query main this PR DuckDB
q01 bare IN 533 ms 2 ms 2 ms
q02 COALESCE over IN 1352 ms 3 ms 2 ms
q03 correlated IN, equality 14 ms 3 ms 3 ms
q04 two IN columns 1026 ms 3 ms 2 ms
q05 bare NOT IN 646 ms 1 ms 2 ms
q06 correlated EXISTS 4 ms 2 ms 2 ms
q07 correlated IN, non-equality 117 ms 93 ms 458 ms

All three engines give the same result for all seven queries, and the counts are the ones in the suite's checked-in result files.

For the four quadratic shapes, q01, q02, q04 and q05, main is 270x to 680x slower than DuckDB. This PR puts them at DuckDB's cost. These times are at the 1 ms resolution of both command line tools, so the exact factor is approximate; the size of the difference is not.

q07 now uses one null-aware mark join with the < correlation as a residual filter. Measured again after the rebase, median of 7 runs at 30,000 rows: main at 39ca2c74a9 78 ms, this PR 3 ms, DuckDB 404 ms. The counts are the same in all three.

A second run at 100,000 rows per table gives the same picture: main takes 2741 ms to 6355 ms for q01, q02, q04 and q05, this PR takes 2 ms to 4 ms and DuckDB 3 ms to 4 ms. All three engines again give the same counts.

How to run it

The queries are the suite's own benchmarks/sql_benchmarks/projection_subquery/queries/q0*.sql, with an alias added to the derived table. The load SQL needs one change for DuckDB, because generate_series gives a column of that name there and a column named value in DataFusion:

CREATE TABLE outer_t AS
SELECT
  CAST(value AS INT) AS id,
  CAST(value % 1000 AS INT) AS z
FROM generate_series(1, 30000) AS g(value);

CREATE TABLE inner_t AS
SELECT
  CASE WHEN value % 97 = 0 THEN NULL ELSE CAST(value * 2 AS INT) END AS id,
  CAST(value % 1000 AS INT) AS z
FROM generate_series(1, 30000) AS g(value);

datafusion-cli reports the time of each statement as Elapsed, and duckdb reports it as Run Time (s): real after .timer on.

What is the testing strategy for this PR?

  • Unit tests in datafusion/optimizer/src/decorrelate_predicate_subquery.rs. The updated snapshots show a single mark join. New tests cover a residual filter, NOT IN, nullable key expressions, a correlation that repeats the IN predicate, and the same correlation below a ROLLUP. The last one fails without the clearing in item 2.
  • The new sqllogictest cases in subquery_projection.slt listed above.
  • The full sqllogictest suite, the optimizer crate and workspace clippy are green locally.
  • The benchmark numbers above.

Are there any user-facing changes?

Plans for projected IN and NOT IN subqueries are much faster. There are no changes to any public API, and no documentation change is needed.

Some query results change, and each change is a correction. DuckDB 1.5.2 gives the new results:

🤖 Generated with Claude Code

@github-actions github-actions Bot added optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt) labels Sep 15, 2026
@adriangb
adriangb force-pushed the in-exists-subquery-projection branch from 9e94b59 to 04f3d7d Compare September 15, 2026 19:17
@adriangb adriangb changed the title feat: support InSubquery and Exists in Projection expressions perf: use one null-aware mark join for hashable IN subqueries in projections Sep 15, 2026
@codecov-commenter

codecov-commenter commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.61272% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.57%. Comparing base (4e19f25) to head (210d4e5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...on/optimizer/src/decorrelate_predicate_subquery.rs 77.60% 29 Missing and 44 partials ⚠️
datafusion/optimizer/src/decorrelate.rs 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25338      +/-   ##
==========================================
- Coverage   82.57%   82.57%   -0.01%     
==========================================
  Files        1142     1142              
  Lines      440958   441219     +261     
  Branches   440958   441219     +261     
==========================================
+ Hits       364141   364322     +181     
- Misses      54806    54854      +48     
- Partials    22011    22043      +32     

☔ 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.

@adriangb

Copy link
Copy Markdown
Contributor Author

@kosiew since you reviewed #24972 would you mind taking a look here? cc @neilconway since you were involved in #21363

@adriangb

Copy link
Copy Markdown
Contributor Author

run benchmarks

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5687257997-2377-hfl77 6.12.94+ #1 SMP Tue Aug 4 08:44:15 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 in-exists-subquery-projection (04f3d7d) to 22651d2 (merge-base) diff

Run configuration
run benchmark clickbench_partitioned

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-c5687257997-2378-jf2f2 6.12.94+ #1 SMP Tue Aug 4 08:44:15 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 in-exists-subquery-projection (04f3d7d) to 22651d2 (merge-base) diff

Run configuration
run benchmark tpcds

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-c5687257997-2379-szrcx 6.12.94+ #1 SMP Tue Aug 4 08:44:15 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 in-exists-subquery-projection (04f3d7d) to 22651d2 (merge-base) diff

Run configuration
run benchmark tpch

Results will be posted here when complete


File an issue against this benchmark runner

Copilot AI 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.

🟡 Changes recommended

Expression-level nullability can be missed, causing projected IN to return false instead of UNKNOWN.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Optimizes projected IN/NOT IN subqueries by using a single null-aware mark join when possible.

Changes:

  • Tracks whether mark joins preserve three-valued logic.
  • Retains three-join fallback for residual predicates.
  • Adds plan and NULL-semantics regression tests.
File summaries
File Description
datafusion/optimizer/src/decorrelate_predicate_subquery.rs Implements single-mark-join optimization.
datafusion/sqllogictest/test_files/subquery_projection.slt Tests plans and NULL semantics.
datafusion/sqllogictest/test_files/projection_pushdown.slt Updates alias-collision regression coverage.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread datafusion/optimizer/src/decorrelate_predicate_subquery.rs Outdated
@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

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

Comparing in-exists-subquery-projection (04f3d7d) to 22651d2 (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 in-exists-subquery-projection
--------------------
Benchmark tpch_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query     ┃     HEAD ┃ in-exists-subquery-projection ┃    Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ QQuery 1  │ 38.63 ms │                      38.67 ms │ no change │
│ QQuery 2  │ 18.88 ms │                      18.89 ms │ no change │
│ QQuery 3  │ 29.09 ms │                      28.70 ms │ no change │
│ QQuery 4  │ 17.61 ms │                      17.66 ms │ no change │
│ QQuery 5  │ 35.97 ms │                      35.89 ms │ no change │
│ QQuery 6  │ 16.11 ms │                      16.08 ms │ no change │
│ QQuery 7  │ 41.79 ms │                      42.43 ms │ no change │
│ QQuery 8  │ 41.38 ms │                      41.02 ms │ no change │
│ QQuery 9  │ 49.43 ms │                      50.09 ms │ no change │
│ QQuery 10 │ 42.28 ms │                      42.51 ms │ no change │
│ QQuery 11 │ 13.59 ms │                      13.64 ms │ no change │
│ QQuery 12 │ 24.12 ms │                      23.91 ms │ no change │
│ QQuery 13 │ 39.91 ms │                      39.40 ms │ no change │
│ QQuery 14 │ 24.47 ms │                      24.49 ms │ no change │
│ QQuery 15 │ 30.82 ms │                      30.91 ms │ no change │
│ QQuery 16 │ 13.81 ms │                      13.74 ms │ no change │
│ QQuery 17 │ 71.46 ms │                      71.66 ms │ no change │
│ QQuery 18 │ 61.89 ms │                      61.37 ms │ no change │
│ QQuery 19 │ 33.16 ms │                      32.94 ms │ no change │
│ QQuery 20 │ 31.88 ms │                      31.82 ms │ no change │
│ QQuery 21 │ 56.05 ms │                      56.27 ms │ no change │
│ QQuery 22 │ 14.02 ms │                      14.35 ms │ no change │
└───────────┴──────────┴───────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Benchmark Summary                            ┃          ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 746.36ms │
│ Total Time (in-exists-subquery-projection)   │ 746.43ms │
│ Average Time (HEAD)                          │  33.93ms │
│ Average Time (in-exists-subquery-projection) │  33.93ms │
│ 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 in-exists-subquery-projection
--------------------
Benchmark tpch_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query     ┃                           HEAD ┃  in-exists-subquery-projection ┃    Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ QQuery 1  │ 38.63 / 39.52 ±1.16 / 41.67 ms │ 38.67 / 39.77 ±1.30 / 41.53 ms │ no change │
│ QQuery 2  │ 18.88 / 19.34 ±0.37 / 19.82 ms │ 18.89 / 19.20 ±0.25 / 19.54 ms │ no change │
│ QQuery 3  │ 29.09 / 29.24 ±0.15 / 29.53 ms │ 28.70 / 29.08 ±0.39 / 29.83 ms │ no change │
│ QQuery 4  │ 17.61 / 18.03 ±0.53 / 19.08 ms │ 17.66 / 18.11 ±0.50 / 19.05 ms │ no change │
│ QQuery 5  │ 35.97 / 36.48 ±0.37 / 37.10 ms │ 35.89 / 36.88 ±0.89 / 38.48 ms │ no change │
│ QQuery 6  │ 16.11 / 16.79 ±1.02 / 18.81 ms │ 16.08 / 17.25 ±0.85 / 18.41 ms │ no change │
│ QQuery 7  │ 41.79 / 42.52 ±0.42 / 42.99 ms │ 42.43 / 43.26 ±0.90 / 44.86 ms │ no change │
│ QQuery 8  │ 41.38 / 43.00 ±1.54 / 45.66 ms │ 41.02 / 41.31 ±0.24 / 41.66 ms │ no change │
│ QQuery 9  │ 49.43 / 50.56 ±1.33 / 52.85 ms │ 50.09 / 51.17 ±1.05 / 53.08 ms │ no change │
│ QQuery 10 │ 42.28 / 42.49 ±0.17 / 42.68 ms │ 42.51 / 43.24 ±0.57 / 43.98 ms │ no change │
│ QQuery 11 │ 13.59 / 13.81 ±0.16 / 14.02 ms │ 13.64 / 13.85 ±0.14 / 14.03 ms │ no change │
│ QQuery 12 │ 24.12 / 24.37 ±0.13 / 24.50 ms │ 23.91 / 24.41 ±0.37 / 24.93 ms │ no change │
│ QQuery 13 │ 39.91 / 41.31 ±1.53 / 43.35 ms │ 39.40 / 40.70 ±1.00 / 41.86 ms │ no change │
│ QQuery 14 │ 24.47 / 25.13 ±0.96 / 27.01 ms │ 24.49 / 24.67 ±0.15 / 24.93 ms │ no change │
│ QQuery 15 │ 30.82 / 31.39 ±0.59 / 32.26 ms │ 30.91 / 31.13 ±0.27 / 31.62 ms │ no change │
│ QQuery 16 │ 13.81 / 13.95 ±0.13 / 14.14 ms │ 13.74 / 13.97 ±0.12 / 14.06 ms │ no change │
│ QQuery 17 │ 71.46 / 74.32 ±1.63 / 76.08 ms │ 71.66 / 73.25 ±1.21 / 75.02 ms │ no change │
│ QQuery 18 │ 61.89 / 62.58 ±0.41 / 63.17 ms │ 61.37 / 62.17 ±0.73 / 63.50 ms │ no change │
│ QQuery 19 │ 33.16 / 33.49 ±0.22 / 33.81 ms │ 32.94 / 33.69 ±0.74 / 35.10 ms │ no change │
│ QQuery 20 │ 31.88 / 32.55 ±0.64 / 33.64 ms │ 31.82 / 32.57 ±0.59 / 33.33 ms │ no change │
│ QQuery 21 │ 56.05 / 57.28 ±1.38 / 59.30 ms │ 56.27 / 57.90 ±1.34 / 60.13 ms │ no change │
│ QQuery 22 │ 14.02 / 14.14 ±0.12 / 14.36 ms │ 14.35 / 14.60 ±0.21 / 14.98 ms │ no change │
└───────────┴────────────────────────────────┴────────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Benchmark Summary                            ┃          ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 762.27ms │
│ Total Time (in-exists-subquery-projection)   │ 762.16ms │
│ Average Time (HEAD)                          │  34.65ms │
│ Average Time (in-exists-subquery-projection) │  34.64ms │
│ 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 479.8 MiB
CPU user 21.3s
CPU sys 1.8s
Peak spill 0 B

tpch — branch

Metric Value
Wall time 5.0s
Peak memory 1.2 GiB
Avg memory 502.6 MiB
CPU user 21.5s
CPU sys 1.7s
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 in-exists-subquery-projection (04f3d7d) to 22651d2 (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 in-exists-subquery-projection
--------------------
Benchmark tpcds_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃       HEAD ┃ in-exists-subquery-projection ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 1  │    5.60 ms │                       5.65 ms │     no change │
│ QQuery 2  │   80.59 ms │                      81.35 ms │     no change │
│ QQuery 3  │   28.90 ms │                      29.26 ms │     no change │
│ QQuery 4  │  487.96 ms │                     478.89 ms │     no change │
│ QQuery 5  │   51.85 ms │                      52.23 ms │     no change │
│ QQuery 6  │   35.91 ms │                      36.32 ms │     no change │
│ QQuery 7  │   74.41 ms │                      74.97 ms │     no change │
│ QQuery 8  │   36.57 ms │                      36.57 ms │     no change │
│ QQuery 9  │   52.90 ms │                      50.12 ms │ +1.06x faster │
│ QQuery 10 │   61.39 ms │                      62.18 ms │     no change │
│ QQuery 11 │  294.53 ms │                     294.07 ms │     no change │
│ QQuery 12 │   28.45 ms │                      28.62 ms │     no change │
│ QQuery 13 │  118.07 ms │                     117.72 ms │     no change │
│ QQuery 14 │  417.71 ms │                     419.36 ms │     no change │
│ QQuery 15 │   56.68 ms │                      56.43 ms │     no change │
│ QQuery 16 │    6.49 ms │                       6.75 ms │     no change │
│ QQuery 17 │   80.44 ms │                      80.63 ms │     no change │
│ QQuery 18 │  104.14 ms │                     104.49 ms │     no change │
│ QQuery 19 │   41.05 ms │                      41.38 ms │     no change │
│ QQuery 20 │   35.48 ms │                      35.63 ms │     no change │
│ QQuery 21 │   17.37 ms │                      17.41 ms │     no change │
│ QQuery 22 │   62.65 ms │                      62.65 ms │     no change │
│ QQuery 23 │  311.45 ms │                     313.55 ms │     no change │
│ QQuery 24 │  193.56 ms │                     195.97 ms │     no change │
│ QQuery 25 │  111.06 ms │                     110.98 ms │     no change │
│ QQuery 26 │   47.98 ms │                      48.59 ms │     no change │
│ QQuery 27 │    6.17 ms │                       6.04 ms │     no change │
│ QQuery 28 │   60.84 ms │                      55.74 ms │ +1.09x faster │
│ QQuery 29 │   96.72 ms │                      98.50 ms │     no change │
│ QQuery 30 │   32.34 ms │                      32.48 ms │     no change │
│ QQuery 31 │  109.82 ms │                     110.34 ms │     no change │
│ QQuery 32 │   19.87 ms │                      20.13 ms │     no change │
│ QQuery 33 │   37.80 ms │                      37.68 ms │     no change │
│ QQuery 34 │    9.98 ms │                       9.78 ms │     no change │
│ QQuery 35 │   71.39 ms │                      71.49 ms │     no change │
│ QQuery 36 │    5.76 ms │                       5.79 ms │     no change │
│ QQuery 37 │    6.80 ms │                       6.82 ms │     no change │
│ QQuery 38 │   61.31 ms │                      62.27 ms │     no change │
│ QQuery 39 │   89.18 ms │                      88.89 ms │     no change │
│ QQuery 40 │   23.34 ms │                      23.42 ms │     no change │
│ QQuery 41 │   11.06 ms │                      11.15 ms │     no change │
│ QQuery 42 │   23.47 ms │                      23.93 ms │     no change │
│ QQuery 43 │    5.00 ms │                       5.17 ms │     no change │
│ QQuery 44 │    9.21 ms │                       9.38 ms │     no change │
│ QQuery 45 │   38.37 ms │                      38.71 ms │     no change │
│ QQuery 46 │   11.83 ms │                      12.07 ms │     no change │
│ QQuery 47 │  229.49 ms │                     227.91 ms │     no change │
│ QQuery 48 │   95.49 ms │                      95.66 ms │     no change │
│ QQuery 49 │   71.02 ms │                      70.97 ms │     no change │
│ QQuery 50 │   59.20 ms │                      59.06 ms │     no change │
│ QQuery 51 │   90.14 ms │                      90.72 ms │     no change │
│ QQuery 52 │   24.36 ms │                      23.78 ms │     no change │
│ QQuery 53 │   29.03 ms │                      28.95 ms │     no change │
│ QQuery 54 │   54.51 ms │                      53.91 ms │     no change │
│ QQuery 55 │   23.29 ms │                      23.24 ms │     no change │
│ QQuery 56 │   39.13 ms │                      38.56 ms │     no change │
│ QQuery 57 │  175.70 ms │                     175.83 ms │     no change │
│ QQuery 58 │  112.64 ms │                     112.07 ms │     no change │
│ QQuery 59 │  117.56 ms │                     117.71 ms │     no change │
│ QQuery 60 │   39.96 ms │                      38.77 ms │     no change │
│ QQuery 61 │   12.07 ms │                      12.07 ms │     no change │
│ QQuery 62 │   46.26 ms │                      46.11 ms │     no change │
│ QQuery 63 │   29.16 ms │                      29.36 ms │     no change │
│ QQuery 64 │  367.34 ms │                     369.03 ms │     no change │
│ QQuery 65 │  123.86 ms │                     123.68 ms │     no change │
│ QQuery 66 │   79.04 ms │                      78.56 ms │     no change │
│ QQuery 67 │  243.99 ms │                     238.00 ms │     no change │
│ QQuery 68 │   11.88 ms │                      11.84 ms │     no change │
│ QQuery 69 │   55.58 ms │                      56.02 ms │     no change │
│ QQuery 70 │  104.47 ms │                     104.34 ms │     no change │
│ QQuery 71 │   35.03 ms │                      35.19 ms │     no change │
│ QQuery 72 │ 1822.26 ms │                    1777.64 ms │     no change │
│ QQuery 73 │   10.17 ms │                       9.71 ms │     no change │
│ QQuery 74 │  170.78 ms │                     168.81 ms │     no change │
│ QQuery 75 │  146.05 ms │                     146.55 ms │     no change │
│ QQuery 76 │   34.72 ms │                      35.14 ms │     no change │
│ QQuery 77 │   61.54 ms │                      61.06 ms │     no change │
│ QQuery 78 │  220.73 ms │                     222.09 ms │     no change │
│ QQuery 79 │   66.84 ms │                      66.69 ms │     no change │
│ QQuery 80 │   98.23 ms │                      98.75 ms │     no change │
│ QQuery 81 │   25.74 ms │                      25.78 ms │     no change │
│ QQuery 82 │   16.12 ms │                      16.12 ms │     no change │
│ QQuery 83 │   33.86 ms │                      34.05 ms │     no change │
│ QQuery 84 │   29.25 ms │                      29.34 ms │     no change │
│ QQuery 85 │  103.62 ms │                     102.10 ms │     no change │
│ QQuery 86 │   24.73 ms │                      24.96 ms │     no change │
│ QQuery 87 │   61.25 ms │                      61.30 ms │     no change │
│ QQuery 88 │   63.01 ms │                      63.01 ms │     no change │
│ QQuery 89 │   35.78 ms │                      35.44 ms │     no change │
│ QQuery 90 │   16.72 ms │                      16.78 ms │     no change │
│ QQuery 91 │   44.40 ms │                      45.11 ms │     no change │
│ QQuery 92 │   28.51 ms │                      28.62 ms │     no change │
│ QQuery 93 │   49.61 ms │                      49.59 ms │     no change │
│ QQuery 94 │   37.29 ms │                      37.34 ms │     no change │
│ QQuery 95 │   79.36 ms │                      80.77 ms │     no change │
│ QQuery 96 │   23.56 ms │                      23.77 ms │     no change │
│ QQuery 97 │   50.68 ms │                      51.16 ms │     no change │
│ QQuery 98 │   42.82 ms │                      42.27 ms │     no change │
│ QQuery 99 │   70.10 ms │                      70.21 ms │     no change │
└───────────┴────────────┴───────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 9311.40ms │
│ Total Time (in-exists-subquery-projection)   │ 9257.05ms │
│ Average Time (HEAD)                          │   94.05ms │
│ Average Time (in-exists-subquery-projection) │   93.51ms │
│ Queries Faster                               │         2 │
│ Queries Slower                               │         0 │
│ Queries with No Change                       │        97 │
│ Queries with Failure                         │         0 │
└──────────────────────────────────────────────┴───────────┘

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

Comparing HEAD and in-exists-subquery-projection
--------------------
Benchmark tpcds_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃                                  HEAD ┃         in-exists-subquery-projection ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 1  │           5.60 / 6.16 ±0.94 / 8.03 ms │           5.65 / 6.18 ±0.91 / 7.99 ms │     no change │
│ QQuery 2  │        80.59 / 80.93 ±0.23 / 81.18 ms │        81.35 / 81.73 ±0.25 / 82.02 ms │     no change │
│ QQuery 3  │        28.90 / 29.35 ±0.33 / 29.80 ms │        29.26 / 29.34 ±0.06 / 29.43 ms │     no change │
│ QQuery 4  │     487.96 / 496.44 ±5.70 / 505.73 ms │    478.89 / 502.16 ±29.50 / 560.43 ms │     no change │
│ QQuery 5  │        51.85 / 52.25 ±0.40 / 52.85 ms │        52.23 / 52.50 ±0.40 / 53.30 ms │     no change │
│ QQuery 6  │        35.91 / 36.43 ±0.48 / 37.22 ms │        36.32 / 36.52 ±0.22 / 36.95 ms │     no change │
│ QQuery 7  │        74.41 / 74.75 ±0.27 / 75.06 ms │        74.97 / 75.34 ±0.39 / 75.95 ms │     no change │
│ QQuery 8  │        36.57 / 37.64 ±1.90 / 41.44 ms │        36.57 / 37.00 ±0.42 / 37.76 ms │     no change │
│ QQuery 9  │        52.90 / 54.38 ±1.50 / 56.45 ms │        50.12 / 53.16 ±1.82 / 55.86 ms │     no change │
│ QQuery 10 │        61.39 / 61.75 ±0.23 / 62.04 ms │        62.18 / 62.51 ±0.26 / 62.93 ms │     no change │
│ QQuery 11 │     294.53 / 301.47 ±4.35 / 307.29 ms │     294.07 / 298.31 ±2.82 / 302.62 ms │     no change │
│ QQuery 12 │        28.45 / 28.88 ±0.30 / 29.17 ms │        28.62 / 29.09 ±0.37 / 29.54 ms │     no change │
│ QQuery 13 │     118.07 / 118.83 ±0.43 / 119.26 ms │     117.72 / 118.29 ±0.42 / 118.86 ms │     no change │
│ QQuery 14 │     417.71 / 419.98 ±2.38 / 424.01 ms │     419.36 / 423.04 ±1.92 / 424.71 ms │     no change │
│ QQuery 15 │        56.68 / 57.25 ±0.46 / 58.07 ms │        56.43 / 57.25 ±0.86 / 58.58 ms │     no change │
│ QQuery 16 │           6.49 / 6.69 ±0.25 / 7.18 ms │           6.75 / 6.88 ±0.19 / 7.26 ms │     no change │
│ QQuery 17 │        80.44 / 82.48 ±1.96 / 85.25 ms │        80.63 / 82.40 ±2.19 / 86.71 ms │     no change │
│ QQuery 18 │     104.14 / 105.33 ±0.87 / 106.77 ms │     104.49 / 105.68 ±0.89 / 107.05 ms │     no change │
│ QQuery 19 │        41.05 / 41.44 ±0.31 / 41.85 ms │        41.38 / 41.88 ±0.66 / 43.15 ms │     no change │
│ QQuery 20 │        35.48 / 36.25 ±0.47 / 36.93 ms │        35.63 / 36.12 ±0.27 / 36.41 ms │     no change │
│ QQuery 21 │        17.37 / 17.62 ±0.29 / 18.14 ms │        17.41 / 18.16 ±0.80 / 19.43 ms │     no change │
│ QQuery 22 │        62.65 / 63.70 ±0.61 / 64.49 ms │        62.65 / 64.16 ±1.48 / 66.81 ms │     no change │
│ QQuery 23 │     311.45 / 313.16 ±1.07 / 314.32 ms │     313.55 / 316.48 ±2.56 / 321.14 ms │     no change │
│ QQuery 24 │     193.56 / 197.05 ±3.40 / 203.40 ms │     195.97 / 200.42 ±4.43 / 208.12 ms │     no change │
│ QQuery 25 │     111.06 / 111.40 ±0.31 / 111.83 ms │     110.98 / 114.24 ±2.96 / 118.79 ms │     no change │
│ QQuery 26 │        47.98 / 48.72 ±0.40 / 49.09 ms │        48.59 / 49.01 ±0.22 / 49.22 ms │     no change │
│ QQuery 27 │           6.17 / 6.35 ±0.22 / 6.77 ms │           6.04 / 6.34 ±0.31 / 6.94 ms │     no change │
│ QQuery 28 │        60.84 / 62.11 ±1.90 / 65.87 ms │        55.74 / 61.02 ±4.23 / 68.69 ms │     no change │
│ QQuery 29 │        96.72 / 98.38 ±0.99 / 99.76 ms │      98.50 / 100.69 ±1.48 / 102.68 ms │     no change │
│ QQuery 30 │        32.34 / 32.47 ±0.12 / 32.66 ms │        32.48 / 32.76 ±0.16 / 32.93 ms │     no change │
│ QQuery 31 │     109.82 / 112.21 ±2.49 / 116.93 ms │     110.34 / 112.85 ±2.42 / 117.05 ms │     no change │
│ QQuery 32 │        19.87 / 20.89 ±1.15 / 23.11 ms │        20.13 / 20.37 ±0.25 / 20.81 ms │     no change │
│ QQuery 33 │        37.80 / 38.04 ±0.13 / 38.17 ms │        37.68 / 38.03 ±0.28 / 38.46 ms │     no change │
│ QQuery 34 │         9.98 / 10.16 ±0.17 / 10.39 ms │         9.78 / 10.06 ±0.22 / 10.38 ms │     no change │
│ QQuery 35 │        71.39 / 71.98 ±0.53 / 72.66 ms │        71.49 / 71.97 ±0.34 / 72.53 ms │     no change │
│ QQuery 36 │          5.76 / 6.64 ±1.71 / 10.06 ms │           5.79 / 5.93 ±0.16 / 6.24 ms │ +1.12x faster │
│ QQuery 37 │           6.80 / 6.92 ±0.11 / 7.12 ms │          6.82 / 7.78 ±1.73 / 11.23 ms │  1.12x slower │
│ QQuery 38 │        61.31 / 62.30 ±0.95 / 63.95 ms │        62.27 / 62.71 ±0.26 / 63.01 ms │     no change │
│ QQuery 39 │        89.18 / 89.90 ±0.54 / 90.66 ms │        88.89 / 89.44 ±0.66 / 90.73 ms │     no change │
│ QQuery 40 │        23.34 / 23.77 ±0.31 / 24.18 ms │        23.42 / 23.87 ±0.33 / 24.44 ms │     no change │
│ QQuery 41 │        11.06 / 11.18 ±0.18 / 11.53 ms │        11.15 / 11.33 ±0.13 / 11.49 ms │     no change │
│ QQuery 42 │        23.47 / 24.23 ±0.90 / 25.97 ms │        23.93 / 24.48 ±0.38 / 25.10 ms │     no change │
│ QQuery 43 │           5.00 / 5.14 ±0.18 / 5.51 ms │           5.17 / 5.27 ±0.14 / 5.55 ms │     no change │
│ QQuery 44 │           9.21 / 9.30 ±0.08 / 9.45 ms │           9.38 / 9.51 ±0.09 / 9.64 ms │     no change │
│ QQuery 45 │        38.37 / 39.46 ±1.60 / 42.63 ms │        38.71 / 40.02 ±1.17 / 41.86 ms │     no change │
│ QQuery 46 │        11.83 / 12.09 ±0.22 / 12.40 ms │        12.07 / 12.39 ±0.25 / 12.74 ms │     no change │
│ QQuery 47 │     229.49 / 229.84 ±0.39 / 230.59 ms │     227.91 / 231.32 ±3.68 / 236.85 ms │     no change │
│ QQuery 48 │        95.49 / 96.22 ±1.12 / 98.46 ms │        95.66 / 96.09 ±0.30 / 96.52 ms │     no change │
│ QQuery 49 │        71.02 / 72.42 ±2.22 / 76.83 ms │        70.97 / 71.43 ±0.51 / 72.33 ms │     no change │
│ QQuery 50 │        59.20 / 59.93 ±0.45 / 60.52 ms │        59.06 / 59.43 ±0.44 / 60.28 ms │     no change │
│ QQuery 51 │        90.14 / 93.55 ±2.19 / 96.44 ms │        90.72 / 93.26 ±2.37 / 97.03 ms │     no change │
│ QQuery 52 │        24.36 / 24.51 ±0.13 / 24.74 ms │        23.78 / 24.36 ±0.49 / 25.28 ms │     no change │
│ QQuery 53 │        29.03 / 29.74 ±1.00 / 31.74 ms │        28.95 / 29.19 ±0.19 / 29.54 ms │     no change │
│ QQuery 54 │        54.51 / 54.73 ±0.14 / 54.89 ms │        53.91 / 54.74 ±0.82 / 56.30 ms │     no change │
│ QQuery 55 │        23.29 / 23.84 ±0.44 / 24.47 ms │        23.24 / 23.48 ±0.15 / 23.71 ms │     no change │
│ QQuery 56 │        39.13 / 40.53 ±2.61 / 45.74 ms │        38.56 / 39.15 ±0.37 / 39.64 ms │     no change │
│ QQuery 57 │     175.70 / 176.94 ±0.77 / 177.83 ms │     175.83 / 178.58 ±4.36 / 187.26 ms │     no change │
│ QQuery 58 │     112.64 / 114.34 ±2.16 / 118.54 ms │     112.07 / 114.55 ±2.93 / 120.15 ms │     no change │
│ QQuery 59 │     117.56 / 118.76 ±1.20 / 120.90 ms │     117.71 / 119.23 ±2.49 / 124.19 ms │     no change │
│ QQuery 60 │        39.96 / 40.21 ±0.20 / 40.48 ms │        38.77 / 40.32 ±0.86 / 41.37 ms │     no change │
│ QQuery 61 │        12.07 / 12.24 ±0.22 / 12.68 ms │        12.07 / 12.33 ±0.24 / 12.78 ms │     no change │
│ QQuery 62 │        46.26 / 47.40 ±0.81 / 48.62 ms │        46.11 / 46.46 ±0.30 / 46.86 ms │     no change │
│ QQuery 63 │        29.16 / 29.73 ±0.34 / 30.20 ms │        29.36 / 29.60 ±0.29 / 30.14 ms │     no change │
│ QQuery 64 │     367.34 / 369.85 ±1.93 / 372.82 ms │     369.03 / 372.54 ±3.36 / 378.10 ms │     no change │
│ QQuery 65 │     123.86 / 127.59 ±3.45 / 133.86 ms │     123.68 / 127.02 ±3.06 / 132.52 ms │     no change │
│ QQuery 66 │        79.04 / 80.44 ±0.99 / 81.61 ms │        78.56 / 79.56 ±0.75 / 80.79 ms │     no change │
│ QQuery 67 │     243.99 / 246.65 ±2.21 / 249.38 ms │     238.00 / 244.23 ±3.54 / 248.33 ms │     no change │
│ QQuery 68 │        11.88 / 12.09 ±0.17 / 12.37 ms │        11.84 / 12.01 ±0.21 / 12.43 ms │     no change │
│ QQuery 69 │        55.58 / 57.93 ±3.22 / 64.33 ms │        56.02 / 58.98 ±4.83 / 68.58 ms │     no change │
│ QQuery 70 │     104.47 / 106.04 ±1.75 / 109.44 ms │     104.34 / 105.75 ±1.69 / 108.94 ms │     no change │
│ QQuery 71 │        35.03 / 35.98 ±0.84 / 37.55 ms │        35.19 / 35.49 ±0.29 / 35.97 ms │     no change │
│ QQuery 72 │ 1822.26 / 1889.88 ±46.85 / 1965.09 ms │ 1777.64 / 1857.20 ±66.62 / 1958.83 ms │     no change │
│ QQuery 73 │        10.17 / 11.29 ±1.23 / 13.67 ms │         9.71 / 10.24 ±0.59 / 11.34 ms │ +1.10x faster │
│ QQuery 74 │     170.78 / 173.90 ±1.96 / 176.02 ms │     168.81 / 171.31 ±2.04 / 174.88 ms │     no change │
│ QQuery 75 │     146.05 / 147.30 ±0.79 / 148.33 ms │     146.55 / 147.64 ±1.04 / 148.95 ms │     no change │
│ QQuery 76 │        34.72 / 37.13 ±4.19 / 45.50 ms │        35.14 / 35.49 ±0.24 / 35.83 ms │     no change │
│ QQuery 77 │        61.54 / 62.66 ±1.74 / 66.12 ms │        61.06 / 64.43 ±3.93 / 71.92 ms │     no change │
│ QQuery 78 │     220.73 / 224.58 ±4.49 / 233.34 ms │     222.09 / 226.55 ±4.05 / 232.90 ms │     no change │
│ QQuery 79 │        66.84 / 67.59 ±1.24 / 70.05 ms │        66.69 / 66.93 ±0.26 / 67.43 ms │     no change │
│ QQuery 80 │       98.23 / 99.75 ±0.85 / 100.60 ms │      98.75 / 101.43 ±3.38 / 107.61 ms │     no change │
│ QQuery 81 │        25.74 / 26.01 ±0.15 / 26.19 ms │        25.78 / 26.74 ±1.48 / 29.67 ms │     no change │
│ QQuery 82 │        16.12 / 16.30 ±0.13 / 16.48 ms │        16.12 / 16.66 ±0.62 / 17.79 ms │     no change │
│ QQuery 83 │        33.86 / 34.38 ±0.29 / 34.74 ms │        34.05 / 34.23 ±0.22 / 34.60 ms │     no change │
│ QQuery 84 │        29.25 / 29.51 ±0.18 / 29.68 ms │        29.34 / 29.54 ±0.13 / 29.69 ms │     no change │
│ QQuery 85 │     103.62 / 107.00 ±3.48 / 113.45 ms │     102.10 / 106.56 ±6.19 / 118.60 ms │     no change │
│ QQuery 86 │        24.73 / 25.36 ±0.35 / 25.64 ms │        24.96 / 25.32 ±0.23 / 25.67 ms │     no change │
│ QQuery 87 │        61.25 / 62.04 ±0.42 / 62.48 ms │        61.30 / 62.29 ±0.61 / 63.21 ms │     no change │
│ QQuery 88 │        63.01 / 65.74 ±5.13 / 75.99 ms │        63.01 / 63.47 ±0.35 / 63.80 ms │     no change │
│ QQuery 89 │        35.78 / 36.76 ±1.14 / 38.89 ms │        35.44 / 37.45 ±2.92 / 43.21 ms │     no change │
│ QQuery 90 │        16.72 / 17.01 ±0.17 / 17.23 ms │        16.78 / 17.39 ±0.97 / 19.32 ms │     no change │
│ QQuery 91 │        44.40 / 44.65 ±0.22 / 44.95 ms │        45.11 / 45.49 ±0.38 / 46.22 ms │     no change │
│ QQuery 92 │        28.51 / 29.06 ±0.57 / 29.94 ms │        28.62 / 29.18 ±0.32 / 29.50 ms │     no change │
│ QQuery 93 │        49.61 / 50.91 ±1.13 / 52.59 ms │        49.59 / 50.24 ±0.66 / 51.31 ms │     no change │
│ QQuery 94 │        37.29 / 37.81 ±0.56 / 38.62 ms │        37.34 / 38.63 ±1.88 / 42.35 ms │     no change │
│ QQuery 95 │        79.36 / 81.62 ±2.26 / 85.74 ms │        80.77 / 81.47 ±0.59 / 82.29 ms │     no change │
│ QQuery 96 │        23.56 / 23.89 ±0.25 / 24.32 ms │        23.77 / 24.03 ±0.29 / 24.59 ms │     no change │
│ QQuery 97 │        50.68 / 52.93 ±2.62 / 57.77 ms │        51.16 / 51.52 ±0.53 / 52.57 ms │     no change │
│ QQuery 98 │        42.82 / 43.53 ±0.54 / 44.06 ms │        42.27 / 45.53 ±5.68 / 56.87 ms │     no change │
│ QQuery 99 │        70.10 / 71.05 ±0.64 / 71.87 ms │        70.21 / 71.15 ±0.67 / 72.23 ms │     no change │
└───────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 9495.00ms │
│ Total Time (in-exists-subquery-projection)   │ 9481.85ms │
│ Average Time (HEAD)                          │   95.91ms │
│ Average Time (in-exists-subquery-projection) │   95.78ms │
│ Queries Faster                               │         2 │
│ Queries Slower                               │         1 │
│ Queries with No Change                       │        96 │
│ Queries with Failure                         │         0 │
└──────────────────────────────────────────────┴───────────┘

Resource Usage

tpcds — base (merge-base)

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

tpcds — branch

Metric Value
Wall time 50.0s
Peak memory 1.9 GiB
Avg memory 1.3 GiB
CPU user 204.4s
CPU sys 5.5s
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 in-exists-subquery-projection (04f3d7d) to 22651d2 (merge-base) diff

Run configuration
run benchmark clickbench_partitioned
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 in-exists-subquery-projection
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃       HEAD ┃ in-exists-subquery-projection ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0  │    1.20 ms │                       1.31 ms │  1.09x slower │
│ QQuery 1  │   11.64 ms │                      11.99 ms │     no change │
│ QQuery 2  │   36.24 ms │                      37.10 ms │     no change │
│ QQuery 3  │   33.09 ms │                      32.32 ms │     no change │
│ QQuery 4  │  235.57 ms │                     223.59 ms │ +1.05x faster │
│ QQuery 5  │  307.17 ms │                     274.83 ms │ +1.12x faster │
│ QQuery 6  │    1.41 ms │                       1.26 ms │ +1.12x faster │
│ QQuery 7  │   14.21 ms │                      13.09 ms │ +1.09x faster │
│ QQuery 8  │  384.07 ms │                     328.91 ms │ +1.17x faster │
│ QQuery 9  │  531.68 ms │                     465.07 ms │ +1.14x faster │
│ QQuery 10 │   74.56 ms │                      70.59 ms │ +1.06x faster │
│ QQuery 11 │   87.69 ms │                      81.76 ms │ +1.07x faster │
│ QQuery 12 │  276.65 ms │                     267.67 ms │     no change │
│ QQuery 13 │  360.99 ms │                     377.83 ms │     no change │
│ QQuery 14 │  282.98 ms │                     284.88 ms │     no change │
│ QQuery 15 │  268.82 ms │                     272.32 ms │     no change │
│ QQuery 16 │  623.33 ms │                     698.89 ms │  1.12x slower │
│ QQuery 17 │  624.35 ms │                     712.99 ms │  1.14x slower │
│ QQuery 18 │ 1260.58 ms │                    1297.29 ms │     no change │
│ QQuery 19 │   27.38 ms │                      27.29 ms │     no change │
│ QQuery 20 │  516.29 ms │                     518.94 ms │     no change │
│ QQuery 21 │  516.38 ms │                     518.17 ms │     no change │
│ QQuery 22 │  980.60 ms │                     992.06 ms │     no change │
│ QQuery 23 │ 3060.41 ms │                    3123.45 ms │     no change │
│ QQuery 24 │   41.52 ms │                      41.76 ms │     no change │
│ QQuery 25 │  110.19 ms │                     110.86 ms │     no change │
│ QQuery 26 │   41.57 ms │                      41.77 ms │     no change │
│ QQuery 27 │  512.95 ms │                     516.16 ms │     no change │
│ QQuery 28 │ 2899.49 ms │                    2927.52 ms │     no change │
│ QQuery 29 │   40.72 ms │                      40.95 ms │     no change │
│ QQuery 30 │  307.03 ms │                     313.55 ms │     no change │
│ QQuery 31 │  286.59 ms │                     289.87 ms │     no change │
│ QQuery 32 │  973.30 ms │                    1099.92 ms │  1.13x slower │
│ QQuery 33 │ 1485.01 ms │                    1492.82 ms │     no change │
│ QQuery 34 │ 1482.14 ms │                    1512.53 ms │     no change │
│ QQuery 35 │  278.49 ms │                     280.70 ms │     no change │
│ QQuery 36 │   67.55 ms │                      72.38 ms │  1.07x slower │
│ QQuery 37 │   35.31 ms │                      37.67 ms │  1.07x slower │
│ QQuery 38 │   40.35 ms │                      41.42 ms │     no change │
│ QQuery 39 │  138.13 ms │                     138.14 ms │     no change │
│ QQuery 40 │   13.81 ms │                      14.30 ms │     no change │
│ QQuery 41 │   13.55 ms │                      13.70 ms │     no change │
│ QQuery 42 │   13.03 ms │                      13.19 ms │     no change │
└───────────┴────────────┴───────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 19298.00ms │
│ Total Time (in-exists-subquery-projection)   │ 19632.82ms │
│ Average Time (HEAD)                          │   448.79ms │
│ Average Time (in-exists-subquery-projection) │   456.58ms │
│ Queries Faster                               │          8 │
│ Queries Slower                               │          6 │
│ Queries with No Change                       │         29 │
│ Queries with Failure                         │          0 │
└──────────────────────────────────────────────┴────────────┘

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

Comparing HEAD and in-exists-subquery-projection
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃                                  HEAD ┃         in-exists-subquery-projection ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0  │          1.20 / 3.99 ±5.47 / 14.92 ms │          1.31 / 4.37 ±5.88 / 16.11 ms │  1.10x slower │
│ QQuery 1  │        11.64 / 11.84 ±0.10 / 11.92 ms │        11.99 / 12.47 ±0.39 / 12.96 ms │  1.05x slower │
│ QQuery 2  │        36.24 / 36.69 ±0.27 / 37.08 ms │        37.10 / 37.36 ±0.35 / 38.05 ms │     no change │
│ QQuery 3  │        33.09 / 34.03 ±0.74 / 35.06 ms │        32.32 / 32.89 ±0.39 / 33.30 ms │     no change │
│ QQuery 4  │    235.57 / 260.85 ±12.68 / 268.45 ms │    223.59 / 242.06 ±14.51 / 259.98 ms │ +1.08x faster │
│ QQuery 5  │     307.17 / 311.87 ±3.56 / 317.60 ms │     274.83 / 282.01 ±6.29 / 293.35 ms │ +1.11x faster │
│ QQuery 6  │           1.41 / 1.59 ±0.21 / 1.99 ms │           1.26 / 1.42 ±0.21 / 1.84 ms │ +1.11x faster │
│ QQuery 7  │        14.21 / 14.34 ±0.12 / 14.53 ms │        13.09 / 13.35 ±0.20 / 13.67 ms │ +1.07x faster │
│ QQuery 8  │     384.07 / 389.42 ±3.79 / 395.56 ms │     328.91 / 332.68 ±3.79 / 339.13 ms │ +1.17x faster │
│ QQuery 9  │    531.68 / 545.39 ±10.55 / 564.30 ms │     465.07 / 476.24 ±8.69 / 487.26 ms │ +1.15x faster │
│ QQuery 10 │        74.56 / 75.68 ±1.11 / 77.78 ms │        70.59 / 72.92 ±2.88 / 78.30 ms │     no change │
│ QQuery 11 │        87.69 / 88.16 ±0.47 / 88.89 ms │        81.76 / 82.95 ±0.88 / 84.23 ms │ +1.06x faster │
│ QQuery 12 │    276.65 / 289.47 ±15.63 / 319.97 ms │     267.67 / 276.85 ±7.80 / 289.63 ms │     no change │
│ QQuery 13 │    360.99 / 374.63 ±10.24 / 385.15 ms │    377.83 / 388.21 ±12.07 / 410.08 ms │     no change │
│ QQuery 14 │     282.98 / 287.98 ±4.44 / 295.34 ms │     284.88 / 289.43 ±3.88 / 295.69 ms │     no change │
│ QQuery 15 │     268.82 / 275.63 ±4.21 / 280.65 ms │    272.32 / 284.96 ±10.60 / 303.59 ms │     no change │
│ QQuery 16 │    623.33 / 672.83 ±31.05 / 715.89 ms │    698.89 / 716.77 ±13.10 / 739.28 ms │  1.07x slower │
│ QQuery 17 │    624.35 / 638.11 ±17.47 / 669.64 ms │     712.99 / 727.49 ±8.53 / 739.22 ms │  1.14x slower │
│ QQuery 18 │ 1260.58 / 1293.90 ±34.40 / 1357.34 ms │ 1297.29 / 1357.92 ±66.03 / 1479.93 ms │     no change │
│ QQuery 19 │        27.38 / 31.10 ±6.13 / 43.31 ms │        27.29 / 29.74 ±3.82 / 37.35 ms │     no change │
│ QQuery 20 │    516.29 / 535.09 ±14.36 / 555.11 ms │    518.94 / 532.31 ±14.30 / 558.85 ms │     no change │
│ QQuery 21 │     516.38 / 525.79 ±6.21 / 533.41 ms │    518.17 / 538.28 ±10.56 / 548.29 ms │     no change │
│ QQuery 22 │  980.60 / 1007.40 ±17.01 / 1024.98 ms │  992.06 / 1003.90 ±12.29 / 1026.88 ms │     no change │
│ QQuery 23 │ 3060.41 / 3110.25 ±54.40 / 3201.96 ms │ 3123.45 / 3148.97 ±15.75 / 3165.81 ms │     no change │
│ QQuery 24 │        41.52 / 42.18 ±0.40 / 42.64 ms │       41.76 / 48.91 ±11.10 / 70.62 ms │  1.16x slower │
│ QQuery 25 │     110.19 / 112.79 ±4.02 / 120.76 ms │     110.86 / 113.56 ±4.05 / 121.59 ms │     no change │
│ QQuery 26 │        41.57 / 42.55 ±0.60 / 43.23 ms │        41.77 / 42.86 ±1.25 / 45.23 ms │     no change │
│ QQuery 27 │     512.95 / 520.09 ±5.10 / 528.45 ms │     516.16 / 524.93 ±7.59 / 536.02 ms │     no change │
│ QQuery 28 │ 2899.49 / 3042.84 ±80.83 / 3116.48 ms │ 2927.52 / 2954.34 ±23.04 / 2996.84 ms │     no change │
│ QQuery 29 │        40.72 / 43.40 ±4.65 / 52.68 ms │        40.95 / 41.61 ±0.89 / 43.28 ms │     no change │
│ QQuery 30 │    307.03 / 324.18 ±13.77 / 343.86 ms │     313.55 / 318.15 ±3.37 / 322.72 ms │     no change │
│ QQuery 31 │    286.59 / 314.60 ±17.59 / 337.62 ms │    289.87 / 318.48 ±21.92 / 352.77 ms │     no change │
│ QQuery 32 │  973.30 / 1000.58 ±29.59 / 1047.55 ms │ 1099.92 / 1137.87 ±26.16 / 1173.81 ms │  1.14x slower │
│ QQuery 33 │ 1485.01 / 1530.88 ±57.27 / 1631.87 ms │ 1492.82 / 1624.01 ±94.04 / 1752.34 ms │  1.06x slower │
│ QQuery 34 │ 1482.14 / 1526.91 ±40.79 / 1603.25 ms │ 1512.53 / 1583.94 ±55.35 / 1675.37 ms │     no change │
│ QQuery 35 │    278.49 / 318.32 ±60.53 / 437.89 ms │    280.70 / 306.97 ±28.11 / 344.62 ms │     no change │
│ QQuery 36 │        67.55 / 76.26 ±6.34 / 84.64 ms │        72.38 / 81.78 ±8.00 / 93.60 ms │  1.07x slower │
│ QQuery 37 │        35.31 / 35.84 ±0.49 / 36.47 ms │        37.67 / 38.99 ±1.65 / 42.20 ms │  1.09x slower │
│ QQuery 38 │        40.35 / 43.80 ±2.29 / 47.11 ms │        41.42 / 45.46 ±3.00 / 50.73 ms │     no change │
│ QQuery 39 │     138.13 / 148.09 ±7.76 / 158.89 ms │     138.14 / 154.05 ±9.14 / 163.14 ms │     no change │
│ QQuery 40 │        13.81 / 14.15 ±0.39 / 14.83 ms │        14.30 / 14.74 ±0.40 / 15.39 ms │     no change │
│ QQuery 41 │        13.55 / 14.38 ±1.32 / 17.00 ms │        13.70 / 14.56 ±1.17 / 16.87 ms │     no change │
│ QQuery 42 │        13.03 / 13.76 ±1.07 / 15.87 ms │        13.19 / 13.82 ±0.85 / 15.50 ms │     no change │
└───────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 19981.62ms │
│ Total Time (in-exists-subquery-projection)   │ 20264.57ms │
│ Average Time (HEAD)                          │   464.69ms │
│ Average Time (in-exists-subquery-projection) │   471.27ms │
│ Queries Faster                               │          7 │
│ Queries Slower                               │          9 │
│ Queries with No Change                       │         27 │
│ Queries with Failure                         │          0 │
└──────────────────────────────────────────────┴────────────┘

Resource Usage

clickbench_partitioned — base (merge-base)

Metric Value
Wall time 105.0s
Peak memory 12.5 GiB
Avg memory 4.4 GiB
CPU user 1021.7s
CPU sys 71.6s
Peak spill 0 B

clickbench_partitioned — branch

Metric Value
Wall time 105.0s
Peak memory 11.4 GiB
Avg memory 4.4 GiB
CPU user 1033.5s
CPU sys 77.3s
Peak spill 0 B

File an issue against this benchmark runner

@adriangb

Copy link
Copy Markdown
Contributor Author

run benchmark clickbench_partitioned
changed:
ref: 22651d2

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5688276356-2381-j7bvc 6.12.94+ #1 SMP Tue Aug 4 08:44:15 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 22651d2 (22651d2) to 22651d2 (merge-base) diff

Run configuration
run benchmark clickbench_partitioned
changed:
  ref: "22651d24cc8196f3206e09a36a437eda9e766b87"

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)

Comparing 22651d2 (22651d2) to 22651d2 (merge-base) diff

Run configuration
run benchmark clickbench_partitioned
changed:
  ref: "22651d24cc8196f3206e09a36a437eda9e766b87"
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 in-exists-subquery-projection
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query     ┃       HEAD ┃ in-exists-subquery-projection ┃    Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ QQuery 0  │    1.23 ms │                       1.25 ms │ no change │
│ QQuery 1  │   11.80 ms │                      12.12 ms │ no change │
│ QQuery 2  │   36.51 ms │                      36.42 ms │ no change │
│ QQuery 3  │   31.66 ms │                      31.24 ms │ no change │
│ QQuery 4  │  230.28 ms │                     232.46 ms │ no change │
│ QQuery 5  │  281.64 ms │                     283.62 ms │ no change │
│ QQuery 6  │    1.28 ms │                       1.29 ms │ no change │
│ QQuery 7  │   13.58 ms │                      13.50 ms │ no change │
│ QQuery 8  │  344.46 ms │                     337.76 ms │ no change │
│ QQuery 9  │  478.66 ms │                     467.02 ms │ no change │
│ QQuery 10 │   70.88 ms │                      70.85 ms │ no change │
│ QQuery 11 │   82.35 ms │                      81.44 ms │ no change │
│ QQuery 12 │  280.50 ms │                     277.76 ms │ no change │
│ QQuery 13 │  373.65 ms │                     390.72 ms │ no change │
│ QQuery 14 │  293.06 ms │                     292.45 ms │ no change │
│ QQuery 15 │  280.03 ms │                     279.83 ms │ no change │
│ QQuery 16 │  632.03 ms │                     638.69 ms │ no change │
│ QQuery 17 │  642.74 ms │                     640.74 ms │ no change │
│ QQuery 18 │ 1302.01 ms │                    1296.72 ms │ no change │
│ QQuery 19 │   27.73 ms │                      27.74 ms │ no change │
│ QQuery 20 │  521.43 ms │                     520.87 ms │ no change │
│ QQuery 21 │  524.12 ms │                     517.74 ms │ no change │
│ QQuery 22 │  995.97 ms │                     994.55 ms │ no change │
│ QQuery 23 │ 3077.88 ms │                    3092.56 ms │ no change │
│ QQuery 24 │   41.22 ms │                      41.75 ms │ no change │
│ QQuery 25 │  111.05 ms │                     111.86 ms │ no change │
│ QQuery 26 │   41.72 ms │                      41.84 ms │ no change │
│ QQuery 27 │  517.27 ms │                     514.51 ms │ no change │
│ QQuery 28 │ 2935.48 ms │                    2986.02 ms │ no change │
│ QQuery 29 │   41.56 ms │                      41.62 ms │ no change │
│ QQuery 30 │  320.24 ms │                     322.06 ms │ no change │
│ QQuery 31 │  285.33 ms │                     292.75 ms │ no change │
│ QQuery 32 │  980.39 ms │                     977.72 ms │ no change │
│ QQuery 33 │ 1549.25 ms │                    1522.71 ms │ no change │
│ QQuery 34 │ 1518.11 ms │                    1530.72 ms │ no change │
│ QQuery 35 │  298.07 ms │                     295.86 ms │ no change │
│ QQuery 36 │   67.73 ms │                      67.14 ms │ no change │
│ QQuery 37 │   37.42 ms │                      37.35 ms │ no change │
│ QQuery 38 │   42.89 ms │                      42.16 ms │ no change │
│ QQuery 39 │  151.46 ms │                     145.43 ms │ no change │
│ QQuery 40 │   14.82 ms │                      14.92 ms │ no change │
│ QQuery 41 │   14.32 ms │                      14.31 ms │ no change │
│ QQuery 42 │   13.99 ms │                      13.88 ms │ no change │
└───────────┴────────────┴───────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 19517.83ms │
│ Total Time (in-exists-subquery-projection)   │ 19553.94ms │
│ Average Time (HEAD)                          │   453.90ms │
│ Average Time (in-exists-subquery-projection) │   454.74ms │
│ Queries Faster                               │          0 │
│ Queries Slower                               │          0 │
│ Queries with No Change                       │         43 │
│ Queries with Failure                         │          0 │
└──────────────────────────────────────────────┴────────────┘

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

Comparing HEAD and in-exists-subquery-projection
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃                                  HEAD ┃         in-exists-subquery-projection ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0  │          1.23 / 4.14 ±5.69 / 15.52 ms │          1.25 / 4.11 ±5.60 / 15.30 ms │     no change │
│ QQuery 1  │        11.80 / 12.28 ±0.24 / 12.47 ms │        12.12 / 12.39 ±0.24 / 12.78 ms │     no change │
│ QQuery 2  │        36.51 / 37.00 ±0.37 / 37.63 ms │        36.42 / 36.82 ±0.40 / 37.57 ms │     no change │
│ QQuery 3  │        31.66 / 32.17 ±0.60 / 33.29 ms │        31.24 / 31.73 ±0.43 / 32.32 ms │     no change │
│ QQuery 4  │     230.28 / 236.21 ±3.14 / 238.78 ms │     232.46 / 235.23 ±1.96 / 237.89 ms │     no change │
│ QQuery 5  │     281.64 / 284.45 ±2.94 / 288.70 ms │     283.62 / 285.58 ±3.04 / 291.63 ms │     no change │
│ QQuery 6  │           1.28 / 1.44 ±0.22 / 1.87 ms │           1.29 / 1.44 ±0.22 / 1.86 ms │     no change │
│ QQuery 7  │        13.58 / 13.68 ±0.10 / 13.84 ms │        13.50 / 13.74 ±0.14 / 13.88 ms │     no change │
│ QQuery 8  │     344.46 / 349.07 ±3.26 / 354.56 ms │     337.76 / 342.88 ±2.79 / 345.76 ms │     no change │
│ QQuery 9  │     478.66 / 484.98 ±4.52 / 490.45 ms │    467.02 / 487.49 ±11.49 / 499.46 ms │     no change │
│ QQuery 10 │        70.88 / 72.21 ±1.37 / 73.91 ms │        70.85 / 71.83 ±0.94 / 73.48 ms │     no change │
│ QQuery 11 │        82.35 / 83.14 ±0.67 / 84.20 ms │        81.44 / 81.78 ±0.24 / 82.12 ms │     no change │
│ QQuery 12 │     280.50 / 287.61 ±5.10 / 295.64 ms │     277.76 / 281.25 ±3.21 / 285.28 ms │     no change │
│ QQuery 13 │    373.65 / 390.12 ±13.85 / 411.36 ms │    390.72 / 402.02 ±13.15 / 427.49 ms │     no change │
│ QQuery 14 │     293.06 / 298.84 ±6.45 / 310.29 ms │     292.45 / 298.29 ±3.67 / 303.15 ms │     no change │
│ QQuery 15 │     280.03 / 287.86 ±5.98 / 295.17 ms │    279.83 / 301.78 ±26.76 / 354.12 ms │     no change │
│ QQuery 16 │     632.03 / 638.63 ±5.99 / 649.40 ms │     638.69 / 648.68 ±7.73 / 659.31 ms │     no change │
│ QQuery 17 │     642.74 / 647.91 ±2.97 / 651.19 ms │    640.74 / 654.68 ±11.60 / 673.53 ms │     no change │
│ QQuery 18 │ 1302.01 / 1327.64 ±16.19 / 1344.13 ms │ 1296.72 / 1341.55 ±35.85 / 1405.11 ms │     no change │
│ QQuery 19 │       27.73 / 34.07 ±10.87 / 55.72 ms │        27.74 / 31.57 ±6.75 / 45.04 ms │ +1.08x faster │
│ QQuery 20 │    521.43 / 532.23 ±11.01 / 548.89 ms │     520.87 / 529.17 ±5.58 / 537.94 ms │     no change │
│ QQuery 21 │     524.12 / 527.09 ±3.91 / 534.73 ms │     517.74 / 521.50 ±2.94 / 525.48 ms │     no change │
│ QQuery 22 │  995.97 / 1013.62 ±11.92 / 1032.13 ms │   994.55 / 1006.62 ±8.04 / 1019.90 ms │     no change │
│ QQuery 23 │ 3077.88 / 3134.16 ±45.41 / 3212.40 ms │ 3092.56 / 3136.31 ±41.13 / 3198.64 ms │     no change │
│ QQuery 24 │        41.22 / 45.78 ±3.86 / 50.97 ms │        41.75 / 46.60 ±5.60 / 55.88 ms │     no change │
│ QQuery 25 │     111.05 / 115.05 ±5.04 / 124.83 ms │     111.86 / 120.61 ±6.71 / 129.57 ms │     no change │
│ QQuery 26 │        41.72 / 43.41 ±3.00 / 49.40 ms │        41.84 / 42.89 ±0.83 / 43.89 ms │     no change │
│ QQuery 27 │    517.27 / 528.97 ±11.52 / 550.82 ms │    514.51 / 530.78 ±15.70 / 559.80 ms │     no change │
│ QQuery 28 │ 2935.48 / 2994.83 ±31.60 / 3027.39 ms │ 2986.02 / 3015.90 ±24.81 / 3050.03 ms │     no change │
│ QQuery 29 │        41.56 / 45.52 ±7.33 / 60.17 ms │       41.62 / 52.73 ±14.35 / 77.88 ms │  1.16x slower │
│ QQuery 30 │     320.24 / 325.66 ±5.35 / 333.56 ms │     322.06 / 324.19 ±2.37 / 328.37 ms │     no change │
│ QQuery 31 │    285.33 / 305.41 ±11.08 / 318.18 ms │    292.75 / 307.88 ±12.24 / 323.59 ms │     no change │
│ QQuery 32 │  980.39 / 1000.01 ±13.94 / 1021.61 ms │  977.72 / 1002.94 ±20.72 / 1040.38 ms │     no change │
│ QQuery 33 │  1549.25 / 1563.12 ±9.94 / 1574.46 ms │ 1522.71 / 1574.68 ±78.87 / 1730.83 ms │     no change │
│ QQuery 34 │ 1518.11 / 1553.66 ±25.06 / 1594.87 ms │ 1530.72 / 1581.90 ±44.50 / 1652.42 ms │     no change │
│ QQuery 35 │    298.07 / 329.39 ±51.57 / 432.33 ms │    295.86 / 345.71 ±83.32 / 512.00 ms │     no change │
│ QQuery 36 │        67.73 / 70.94 ±3.15 / 75.73 ms │        67.14 / 73.25 ±4.11 / 79.53 ms │     no change │
│ QQuery 37 │        37.42 / 43.44 ±4.53 / 50.52 ms │        37.35 / 44.63 ±5.37 / 50.99 ms │     no change │
│ QQuery 38 │        42.89 / 45.43 ±2.70 / 50.13 ms │        42.16 / 45.36 ±3.32 / 51.54 ms │     no change │
│ QQuery 39 │     151.46 / 161.45 ±8.09 / 171.66 ms │     145.43 / 155.55 ±5.12 / 158.78 ms │     no change │
│ QQuery 40 │        14.82 / 15.06 ±0.34 / 15.73 ms │        14.92 / 15.37 ±0.30 / 15.77 ms │     no change │
│ QQuery 41 │        14.32 / 14.66 ±0.36 / 15.15 ms │        14.31 / 14.49 ±0.12 / 14.67 ms │     no change │
│ QQuery 42 │        13.99 / 16.79 ±5.28 / 27.35 ms │        13.88 / 14.12 ±0.13 / 14.24 ms │ +1.19x faster │
└───────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 19949.12ms │
│ Total Time (in-exists-subquery-projection)   │ 20068.00ms │
│ Average Time (HEAD)                          │   463.93ms │
│ Average Time (in-exists-subquery-projection) │   466.70ms │
│ Queries Faster                               │          2 │
│ Queries Slower                               │          1 │
│ Queries with No Change                       │         40 │
│ Queries with Failure                         │          0 │
└──────────────────────────────────────────────┴────────────┘

Resource Usage

clickbench_partitioned — base (merge-base)

Metric Value
Wall time 105.0s
Peak memory 11.8 GiB
Avg memory 4.4 GiB
CPU user 1022.0s
CPU sys 73.1s
Peak spill 0 B

clickbench_partitioned — branch

Metric Value
Wall time 105.0s
Peak memory 11.9 GiB
Avg memory 4.5 GiB
CPU user 1019.1s
CPU sys 74.7s
Peak spill 0 B

File an issue against this benchmark runner

@adriangb

Copy link
Copy Markdown
Contributor Author

run benchmark clickbench_partitioned

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5688513263-2382-7p7sn 6.12.94+ #1 SMP Tue Aug 4 08:44:15 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 in-exists-subquery-projection (04f3d7d) to 22651d2 (merge-base) diff

Run configuration
run benchmark clickbench_partitioned

Results will be posted here when complete


File an issue against this benchmark runner

@adriangb

Copy link
Copy Markdown
Contributor Author

I opened #25346 to add benchmarks

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

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

Comparing in-exists-subquery-projection (04f3d7d) to 22651d2 (merge-base) diff

Run configuration
run benchmark clickbench_partitioned
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 in-exists-subquery-projection
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Query     ┃       HEAD ┃ in-exists-subquery-projection ┃       Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ QQuery 0  │    1.23 ms │                       1.24 ms │    no change │
│ QQuery 1  │   12.21 ms │                      12.01 ms │    no change │
│ QQuery 2  │   36.32 ms │                      36.29 ms │    no change │
│ QQuery 3  │   31.15 ms │                      31.16 ms │    no change │
│ QQuery 4  │  234.75 ms │                     233.62 ms │    no change │
│ QQuery 5  │  278.75 ms │                     280.70 ms │    no change │
│ QQuery 6  │    1.27 ms │                       1.30 ms │    no change │
│ QQuery 7  │   13.59 ms │                      13.55 ms │    no change │
│ QQuery 8  │  341.48 ms │                     341.45 ms │    no change │
│ QQuery 9  │  481.47 ms │                     485.36 ms │    no change │
│ QQuery 10 │   70.57 ms │                      72.18 ms │    no change │
│ QQuery 11 │   82.05 ms │                      82.52 ms │    no change │
│ QQuery 12 │  276.59 ms │                     276.56 ms │    no change │
│ QQuery 13 │  381.07 ms │                     370.06 ms │    no change │
│ QQuery 14 │  297.10 ms │                     292.23 ms │    no change │
│ QQuery 15 │  284.37 ms │                     276.79 ms │    no change │
│ QQuery 16 │  632.54 ms │                     645.58 ms │    no change │
│ QQuery 17 │  637.24 ms │                     641.60 ms │    no change │
│ QQuery 18 │ 1315.39 ms │                    1318.01 ms │    no change │
│ QQuery 19 │   27.91 ms │                      27.82 ms │    no change │
│ QQuery 20 │  527.71 ms │                     518.94 ms │    no change │
│ QQuery 21 │  520.45 ms │                     520.10 ms │    no change │
│ QQuery 22 │ 1003.88 ms │                    1000.16 ms │    no change │
│ QQuery 23 │ 3123.06 ms │                    3147.83 ms │    no change │
│ QQuery 24 │   41.51 ms │                      41.59 ms │    no change │
│ QQuery 25 │  110.71 ms │                     112.05 ms │    no change │
│ QQuery 26 │   41.69 ms │                      41.88 ms │    no change │
│ QQuery 27 │  511.40 ms │                     523.35 ms │    no change │
│ QQuery 28 │ 2967.96 ms │                    2958.13 ms │    no change │
│ QQuery 29 │   41.70 ms │                      41.60 ms │    no change │
│ QQuery 30 │  324.31 ms │                     319.53 ms │    no change │
│ QQuery 31 │  296.24 ms │                     285.84 ms │    no change │
│ QQuery 32 │  965.73 ms │                     959.92 ms │    no change │
│ QQuery 33 │ 1511.77 ms │                    1509.37 ms │    no change │
│ QQuery 34 │ 1547.41 ms │                    1502.82 ms │    no change │
│ QQuery 35 │  298.56 ms │                     297.69 ms │    no change │
│ QQuery 36 │   69.13 ms │                      69.63 ms │    no change │
│ QQuery 37 │   37.05 ms │                      37.76 ms │    no change │
│ QQuery 38 │   41.41 ms │                      44.23 ms │ 1.07x slower │
│ QQuery 39 │  157.55 ms │                     162.07 ms │    no change │
│ QQuery 40 │   14.94 ms │                      15.39 ms │    no change │
│ QQuery 41 │   14.36 ms │                      14.12 ms │    no change │
│ QQuery 42 │   13.86 ms │                      13.77 ms │    no change │
└───────────┴────────────┴───────────────────────────────┴──────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 19619.48ms │
│ Total Time (in-exists-subquery-projection)   │ 19577.80ms │
│ Average Time (HEAD)                          │   456.27ms │
│ Average Time (in-exists-subquery-projection) │   455.30ms │
│ Queries Faster                               │          0 │
│ Queries Slower                               │          1 │
│ Queries with No Change                       │         42 │
│ Queries with Failure                         │          0 │
└──────────────────────────────────────────────┴────────────┘

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

Comparing HEAD and in-exists-subquery-projection
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃                                  HEAD ┃         in-exists-subquery-projection ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0  │          1.23 / 4.13 ±5.65 / 15.43 ms │          1.24 / 4.08 ±5.55 / 15.18 ms │     no change │
│ QQuery 1  │        12.21 / 12.42 ±0.13 / 12.56 ms │        12.01 / 12.28 ±0.20 / 12.54 ms │     no change │
│ QQuery 2  │        36.32 / 36.94 ±0.44 / 37.34 ms │        36.29 / 36.98 ±0.68 / 37.92 ms │     no change │
│ QQuery 3  │        31.15 / 32.10 ±0.96 / 33.93 ms │        31.16 / 31.52 ±0.31 / 31.86 ms │     no change │
│ QQuery 4  │     234.75 / 236.69 ±1.35 / 238.23 ms │     233.62 / 235.87 ±2.17 / 239.89 ms │     no change │
│ QQuery 5  │     278.75 / 286.39 ±4.20 / 291.21 ms │     280.70 / 283.73 ±2.28 / 286.99 ms │     no change │
│ QQuery 6  │           1.27 / 1.45 ±0.26 / 1.95 ms │           1.30 / 1.44 ±0.21 / 1.85 ms │     no change │
│ QQuery 7  │        13.59 / 13.80 ±0.16 / 13.96 ms │        13.55 / 13.66 ±0.07 / 13.75 ms │     no change │
│ QQuery 8  │     341.48 / 346.06 ±3.50 / 352.02 ms │     341.45 / 344.53 ±2.28 / 347.87 ms │     no change │
│ QQuery 9  │     481.47 / 490.01 ±7.89 / 502.20 ms │    485.36 / 499.58 ±11.62 / 513.15 ms │     no change │
│ QQuery 10 │        70.57 / 73.16 ±3.82 / 80.60 ms │        72.18 / 72.95 ±0.93 / 74.69 ms │     no change │
│ QQuery 11 │        82.05 / 82.64 ±0.75 / 84.07 ms │        82.52 / 83.42 ±0.59 / 83.98 ms │     no change │
│ QQuery 12 │     276.59 / 280.43 ±2.22 / 283.01 ms │     276.56 / 284.75 ±5.68 / 292.93 ms │     no change │
│ QQuery 13 │     381.07 / 388.29 ±5.62 / 394.64 ms │    370.06 / 402.27 ±26.16 / 444.88 ms │     no change │
│ QQuery 14 │     297.10 / 299.48 ±2.07 / 302.97 ms │    292.23 / 301.89 ±10.69 / 322.13 ms │     no change │
│ QQuery 15 │     284.37 / 289.72 ±5.41 / 297.55 ms │    276.79 / 299.44 ±20.23 / 332.03 ms │     no change │
│ QQuery 16 │     632.54 / 642.46 ±5.98 / 649.04 ms │     645.58 / 651.35 ±4.50 / 656.99 ms │     no change │
│ QQuery 17 │     637.24 / 650.23 ±8.66 / 664.53 ms │    641.60 / 659.72 ±14.45 / 681.17 ms │     no change │
│ QQuery 18 │  1315.39 / 1329.18 ±9.86 / 1342.01 ms │ 1318.01 / 1350.02 ±28.61 / 1393.93 ms │     no change │
│ QQuery 19 │        27.91 / 34.87 ±9.03 / 51.08 ms │        27.82 / 36.09 ±8.37 / 46.78 ms │     no change │
│ QQuery 20 │    527.71 / 537.95 ±12.72 / 560.51 ms │     518.94 / 525.46 ±5.01 / 533.81 ms │     no change │
│ QQuery 21 │     520.45 / 524.73 ±3.61 / 529.87 ms │     520.10 / 528.82 ±6.19 / 537.29 ms │     no change │
│ QQuery 22 │  1003.88 / 1015.29 ±9.23 / 1025.34 ms │ 1000.16 / 1016.75 ±11.77 / 1031.32 ms │     no change │
│ QQuery 23 │ 3123.06 / 3141.06 ±11.84 / 3156.16 ms │ 3147.83 / 3180.12 ±27.22 / 3226.68 ms │     no change │
│ QQuery 24 │        41.51 / 44.98 ±5.16 / 55.03 ms │       41.59 / 53.43 ±11.05 / 71.75 ms │  1.19x slower │
│ QQuery 25 │     110.71 / 113.06 ±1.91 / 116.17 ms │     112.05 / 112.98 ±0.85 / 114.19 ms │     no change │
│ QQuery 26 │        41.69 / 42.11 ±0.37 / 42.70 ms │        41.88 / 44.77 ±3.36 / 51.36 ms │  1.06x slower │
│ QQuery 27 │     511.40 / 528.32 ±8.82 / 535.36 ms │     523.35 / 533.02 ±9.66 / 549.69 ms │     no change │
│ QQuery 28 │ 2967.96 / 3002.02 ±24.68 / 3030.66 ms │ 2958.13 / 2971.94 ±19.53 / 3010.44 ms │     no change │
│ QQuery 29 │        41.70 / 43.88 ±3.61 / 51.04 ms │        41.60 / 46.63 ±9.49 / 65.60 ms │  1.06x slower │
│ QQuery 30 │     324.31 / 327.33 ±4.23 / 335.65 ms │     319.53 / 325.09 ±5.20 / 333.62 ms │     no change │
│ QQuery 31 │     296.24 / 300.97 ±4.50 / 308.38 ms │     285.84 / 296.33 ±7.51 / 309.04 ms │     no change │
│ QQuery 32 │   965.73 / 984.01 ±17.14 / 1005.78 ms │    959.92 / 981.30 ±12.20 / 998.02 ms │     no change │
│ QQuery 33 │ 1511.77 / 1538.27 ±21.78 / 1570.34 ms │ 1509.37 / 1541.23 ±24.15 / 1575.11 ms │     no change │
│ QQuery 34 │ 1547.41 / 1562.41 ±12.65 / 1583.33 ms │ 1502.82 / 1550.39 ±31.07 / 1591.96 ms │     no change │
│ QQuery 35 │     298.56 / 307.26 ±7.35 / 316.35 ms │    297.69 / 315.95 ±29.57 / 374.83 ms │     no change │
│ QQuery 36 │        69.13 / 79.22 ±7.37 / 88.32 ms │        69.63 / 75.64 ±4.59 / 82.57 ms │     no change │
│ QQuery 37 │        37.05 / 40.32 ±3.79 / 47.17 ms │        37.76 / 42.78 ±3.00 / 46.02 ms │  1.06x slower │
│ QQuery 38 │        41.41 / 46.22 ±3.94 / 53.03 ms │        44.23 / 47.60 ±5.67 / 58.87 ms │     no change │
│ QQuery 39 │     157.55 / 162.17 ±3.53 / 167.04 ms │     162.07 / 170.57 ±7.07 / 181.04 ms │  1.05x slower │
│ QQuery 40 │        14.94 / 15.28 ±0.34 / 15.89 ms │        15.39 / 15.62 ±0.17 / 15.81 ms │     no change │
│ QQuery 41 │        14.36 / 16.93 ±4.14 / 25.18 ms │        14.12 / 16.40 ±3.70 / 23.77 ms │     no change │
│ QQuery 42 │        13.86 / 18.11 ±4.63 / 24.06 ms │        13.77 / 13.94 ±0.13 / 14.08 ms │ +1.30x faster │
└───────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                            │ 19922.34ms │
│ Total Time (in-exists-subquery-projection)   │ 20012.34ms │
│ Average Time (HEAD)                          │   463.31ms │
│ Average Time (in-exists-subquery-projection) │   465.40ms │
│ Queries Faster                               │          1 │
│ Queries Slower                               │          5 │
│ Queries with No Change                       │         37 │
│ Queries with Failure                         │          0 │
└──────────────────────────────────────────────┴────────────┘

Resource Usage

clickbench_partitioned — base (merge-base)

Metric Value
Wall time 100.0s
Peak memory 12.1 GiB
Avg memory 4.7 GiB
CPU user 1018.3s
CPU sys 73.9s
Peak spill 0 B

clickbench_partitioned — branch

Metric Value
Wall time 105.0s
Peak memory 10.8 GiB
Avg memory 4.1 GiB
CPU user 1021.0s
CPU sys 75.4s
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.

@adriangb,

Thanks for working on this. Reusing the exact hashable LeftMark join for projected IN / NOT IN is a nice improvement, and the added NULL-semantics coverage is helpful.

I found one blocking issue in the correlated NOT IN path. The expression-level nullability change can now make a LeftAnti join null-aware when there is more than one hash key, but physical planning only supports a single key for null-aware LeftAnti joins. I left a repro and suggested adding an execution regression test below.

I also left one non-blocking suggestion for the empty-subquery boundary on the new single-mark-join path.

&& join_keys_may_be_null(&join_filter, left.schema(), sub_query_alias.schema())?;
// Additionally, if no join key can be NULL on either side, we don't need
// null-aware semantics because NULLs cannot exist in the keys.
let null_aware = if join_type == JoinType::LeftAnti && in_predicate_opt.is_some() {

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 think this introduces a planning failure for correlated NOT IN when the nullable expression key is combined with another correlated equality key.

For example, SELECT id FROM o WHERE NULLIF(id, 1) NOT IN (SELECT id FROM r WHERE r.grp = o.grp) with non-nullable o(id, grp) and r(id, grp) now makes the LeftAnti join null-aware because NULLIF(id, 1) is nullable. The correlation adds grp as a second hash key, so physical planning rejects the resulting join with null_aware LeftAnti joins only support single column join key, got 2 columns.

This looks newly reachable through the expression-level nullability change. Could we preserve the correct correlated NOT IN semantics using a supported fallback or plan shape here? It would also be good to add this query as an execution regression test and assert that only the SQL-true rows are returned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, confirmed. The query failed with got 2 columns. It plans again in fe408e3: a LeftAnti join with more than one key keeps the column test from main.

This does not give the correct result for your NULLIF example. The join has two keys and is not null-aware, so the k = 1 row stays, the same as on main. The test in subquery_projection.slt records this, with a comment that links apache/datafusion#25347. A correct plan needs a null-aware LeftAnti join with more than one key. apache/datafusion#25339 (approved) adds that. When it merges, I will remove this special case and update the expected output to the correct rows. I did not want to add a second fallback plan here, because apache/datafusion#25339 is the fix for this.

05)----ProjectionExec: expr=[CAST(id@0 AS Int64) as r3.id]
06)------DataSourceExec: partitions=1, partition_sizes=[1]

# `NULLIF(id, 1)` is NULL for `id = 1`, and `r3` has no NULL, so the answer is

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.

Could we also add an empty-subquery case for the new single null-aware mark path using a typed nullable key expression, for example NULLIF(id, 1)?

In particular, it would be useful to assert that the NULL-key row produces false, not NULL, when the subquery is empty, and that the plan still uses a single mark join. The existing top-level NULL IN (empty) test covers the SQL result semantics, but it takes the legacy three-join path, so it does not protect this boundary of the new optimization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in fe408e3

@jayzhan211

Copy link
Copy Markdown
Contributor

Thanks @adriangb , here is a suggestion:

Correlated NOT IN with a scalar-function key now fails to plan

The LeftAnti branch now reads nullability from the key expressions. Most scalar UDFs use the default return_field_from_args, which always says the result can be NULL (datafusion/expr/src/udf.rs). So a key like upper(s) over a NOT NULL column now turns on null_aware.

For a correlated NOT IN in WHERE, that join has 2+ keys (value + correlation). HashJoinExec rejects null-aware LeftAnti with more than one key, and null-aware joins can't fall back to a sort-merge join:

CREATE TABLE t1(k INT NOT NULL, s VARCHAR NOT NULL) AS VALUES (1, 'a');
CREATE TABLE t2(k INT NOT NULL, s VARCHAR NOT NULL) AS VALUES (1, 'B');
SELECT * FROM t1 WHERE upper(t1.s) NOT IN (SELECT t2.s FROM t2 WHERE t2.k = t1.k);
-- main: plans a plain LeftAnti and returns (1, 'a')
-- this PR: expected "null_aware LeftAnti joins only support single column join key"

Nullable columns already fail like this on main (#25347), but this change extends the failure to common function keys over non-nullable data. It also pins uncorrelated lower(s) NOT IN (...) over NOT NULL columns to a CollectLeft null-aware join for no correctness gain.

Suggest limiting the expression-level check on the LeftAnti path to the single-key case until #25347 is fixed:

     let null_aware = if join_type == JoinType::LeftAnti && in_predicate_opt.is_some() {
         let (equijoin_keys, residual_filter) = split_eq_and_noneq_join_predicate(
             join_filter.clone(),
             left.schema(),
             sub_query_alias.schema(),
         )?;
-        join_keys_may_be_null(
-            &equijoin_keys,
-            residual_filter.as_ref(),
-            left.schema(),
-            sub_query_alias.schema(),
-        )?
+        if equijoin_keys.len() == 1 {
+            join_keys_may_be_null(
+                &equijoin_keys,
+                residual_filter.as_ref(),
+                left.schema(),
+                sub_query_alias.schema(),
+            )?
+        } else {
+            // Null-aware LeftAnti supports a single key only (#25347); keep the
+            // previous column-based test so correlated NOT IN still plans.
+            join_filter_columns_may_be_null(&join_filter, left.schema(), sub_query_alias.schema())?
+        }
     } else {

Please also add an slt case with the correlated query above.

@adriangb

Copy link
Copy Markdown
Contributor Author

Thanks @jayzhan211, good catch. I applied your suggestion in fe408e3.

@adriangb

Copy link
Copy Markdown
Contributor Author

@jayzhan211 @kosiew could we merge the benchmarks in #25346 before this change so we can look at perf numbers?

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

@adriangb I leave some suggestions

Comment thread datafusion/optimizer/src/decorrelate.rs Outdated
.all(|&e| can_pullup_over_aggregation(e));
let (mut join_filters, subquery_filters) =
find_join_exprs(subquery_filter_exprs)?;
for expr in &join_filters {

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.

correlated_filters comes from find_join_exprs, which strips outer refs → a subquery column sharing the outer value's qualified name is read as the value → join not null-aware → wrong result (regression vs main).

Repro (expected NULL for the NULL row; PR gives false, main gives NULL; the NOT IN filter keeps the NULL row):

create table o(x int, k int) as values (null, 1), (7, 1), (8, 1);
create table t(x int, y int not null) as values (1, 7);
select a.x, a.x in (select a.y from t as a where a.x = b.k) as r
from o as a, (select 1 as k) as b order by 1;
select a.x from o as a, (select 1 as k) as b
where a.x not in (select a.y from t as a where a.x = b.k);

Fix (tested: repro correct, optimizer tests + subquery_projection/null_aware*/joins/subquery slt pass): keep the outer refs and match per side.

-                let (mut join_filters, subquery_filters) =
-                    find_join_exprs(subquery_filter_exprs)?;
-                for expr in &join_filters {
-                    if !self.correlated_filters.contains(expr) {
-                        self.correlated_filters.push(expr.clone());
+                for expr in &subquery_filter_exprs {
+                    if expr.contains_outer() && !self.correlated_filters.contains(expr) {
+                        self.correlated_filters.push((*expr).clone());
                     }
                 }
+                let (mut join_filters, subquery_filters) =
+                    find_join_exprs(subquery_filter_exprs)?;
fn filter_rejects_null(filter: &Expr, key: &Expr, key_is_outer: bool) -> bool {
    let is_key = |side: &Expr| {
        let side = strip_casts(side);
        if key_is_outer {
            side.contains_outer()
                && side.column_refs().is_empty()
                && &strip_outer_reference(side.clone()) == key
        } else {
            !side.contains_outer() && side == key
        }
    };
    match filter {
        Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
            matches!(
                op,
                Operator::Eq
                    | Operator::NotEq
                    | Operator::Lt
                    | Operator::LtEq
                    | Operator::Gt
                    | Operator::GtEq
            ) && (is_key(left) || is_key(right))
        }
        Expr::IsNotNull(expr) => is_key(expr),
        _ => false,
    }
}

Pass true for value_as_written and false for output_expr from InValue::may_be_null_in_scope, and add the repro to subquery_projection.slt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you, this is a real regression. I took your fix in 10ccc3e. correlated_filters now keeps the outer references, and filter_rejects_null matches each side of a conjunct against the side of the join that the key is on. Your two queries are in subquery_projection.slt (tables sh_o and sh_t), and DuckDB 1.5.2 gives the same results: NULL for the NULL row, and 8 for the NOT IN filter.

// row", which is the weaker fact that this case needs, and which the first
// reading implies. `IS NULL` is two-valued on both sides, so neither test
// adds an UNKNOWN of its own.
let unknown_alias = alias.next("__correlated_sq");

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.

Commit 50d6e4a3a (one merged UNKNOWN join) slows the residual fallback ~1.6x: the NLJ now evaluates k < k AND (y IS NULL OR x IS NULL) on every pair; before, one NLJ ran bare k < k and the other had its right side pre-filtered to y IS NULL.

release-nonlto, 200k x 200k, 3 interleaved runs: main 2.80/3.55/3.78 s · bb043f10e (parent) 3.05/3.06/3.67 s · PR 4.74/5.14/6.48 s. EXPLAIN ANALYZE NLJ elapsed_compute: main 21.7 s + 2.7 s, PR 44.7 s.

create table outer_t as select i as id, case when i % 10 = 0 then null else i end as x, i % 100 as k from generate_series(1, 200000) t(i);
create table inner_t as select i as id, case when i % 7 = 0 then null else i * 2 end as y, i % 100 as k from generate_series(1, 200000) t(i);
select id, x in (select y from inner_t where inner_t.k < outer_t.k) as r from outer_t;

Your Q6 number went the other way, so this is data-dependent. Please drop the commit from this PR (the fallback then matches main) and revisit it with #25336.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I removed 50d6e4a from this PR. I also removed 1a9d842, which only fixed an error message that the merged join caused. The fallback for a residual filter is now the same plan as on main.

After the rebase onto #25339, a NOT IN filter with a residual filter no longer uses the fallback. It becomes one null-aware anti join, because that join now applies the residual filter. The nai_res_og plan that #25339 pins stays as it is, and the joins.slt plan is the same as on main again.

@adriangb
adriangb force-pushed the in-exists-subquery-projection branch from 1a9d842 to 5148cd3 Compare September 24, 2026 18:07
Comment on lines +386 to +395
# `NULLIF(k, 1)` is NULL for `k = 1`, and that group of `t2` is not empty, so
# the correct result has no row for `k = 1`. The join has two keys, the value
# and the correlation, and the null-aware `LeftAnti` executor takes one key
# only. The `NOT IN` becomes a null-aware mark join, which takes any number of
# keys, and a filter on the mark. `main` keeps the `k = 1` row, which is
# https://github.com/apache/datafusion/issues/25347.
query I rowsort
SELECT k FROM t1 WHERE NULLIF(t1.k, 1) NOT IN (SELECT t2.k + 10 FROM t2 WHERE t2.k = t1.k);
----
2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Result changed here, and the old value was wrong. This expectation was 1 and 2; it is now 2 only. DuckDB 1.5.2 agrees with the new value.

NULLIF(t1.k, 1) is NULL for the row k = 1. The correlated subquery for that row gives {11}, which is not empty, so NULL NOT IN ({11}) is UNKNOWN and the row must not appear.

The old plan could not say that. A null-aware LeftAnti join takes one key only, and this join has two, the value and the correlation, so the join stayed a plain anti join and kept the row. The NOT IN now becomes a null-aware LeftMark join, which takes any number of keys, plus a filter on the mark.

D SELECT t1.k, NULLIF(t1.k,1) AS key, (SELECT list(t2.k+10) FROM t2 WHERE t2.k=t1.k) AS sub FROM t1 ORDER BY t1.k;
┌───────┬───────┬───────────┐
│   k   │  key  │    sub    │
├───────┼───────┼───────────┤
│     1 │  NULL │ [11]      │
│     2 │     2 │ [12]      │
└───────┴───────┴───────────┘

D SELECT k FROM t1 WHERE NULLIF(t1.k, 1) NOT IN (SELECT t2.k + 10 FROM t2 WHERE t2.k = t1.k);
┌───┐
│ k │
├───┤
│ 2 │
└───┘

This closes the part of #25347 that a single key cannot express.

Comment on lines +443 to +449
# The same shape where the correlated subquery result is not empty for the NULL
# key: `NULL NOT IN ({5})` is UNKNOWN, so `2` is the only row. `main` builds a
# plain anti join here and also keeps `1`.
query I rowsort
SELECT k FROM ra WHERE NULLIF(ra.k, 1) NOT IN (SELECT rb.k FROM rb WHERE rb.z > ra.z);
----
2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Result changed here, and the old value was wrong. This expectation was 1 and 2; it is now 2 only. DuckDB 1.5.2 agrees with the new value.

NULLIF(ra.k, 1) is NULL for the row k = 1, and the correlated subquery for that row gives {5}, which is not empty. NULL NOT IN ({5}) is UNKNOWN, so the row must not appear.

A residual filter stays on this join, and no hash join can mark the UNKNOWN rows of a residual filter. The NOT IN now becomes the three mark joins that materialize its three-valued result, the plan that #24972 already uses for a projected IN.

D SELECT ra.k, NULLIF(ra.k,1) AS key, (SELECT list(rb.k) FROM rb WHERE rb.z > ra.z) AS sub FROM ra ORDER BY ra.k;
┌───────┬───────┬──────────┐
│   k   │  key  │   sub    │
├───────┼───────┼──────────┤
│     1 │  NULL │ [5]      │
│     2 │     2 │ [5]      │
└───────┴───────┴──────────┘

D SELECT k FROM ra WHERE NULLIF(ra.k, 1) NOT IN (SELECT rb.k FROM rb WHERE rb.z > ra.z);
┌───┐
│ k │
├───┤
│ 2 │
└───┘

The row above, with rb.z < ra.z, keeps its expectation of 1 and 2. Its subquery result is empty for the NULL key, and NULL NOT IN (<empty set>) is TRUE.

This is the executor gap in #25336. #25339 fixes the executor, and this shape can go back to one anti join when that lands.

Comment on lines +481 to +500
# The correlation names no column of the subquery, so it stays as a residual
# filter and the subquery is either the whole of `ic` or empty. A NULL in
# `ic.id` then makes the answer UNKNOWN only for the rows whose correlation
# holds. `main` builds a null-aware anti join that does not apply the residual
# when it looks for a NULL, sees one in `ic`, and drops every row. This is the
# shape whose plan `joins.slt` pins.
statement ok
CREATE TABLE oc(id INT, g INT) AS VALUES (1, 1), (2, 0), (NULL, 0);

statement ok
CREATE TABLE ic(id INT) AS VALUES (5), (NULL);

# `g > 0` holds for `id = 1` only, so its subquery is `{5, NULL}` and
# `1 NOT IN {5, NULL}` is UNKNOWN. The other two rows have an empty subquery,
# and `<anything> NOT IN (<empty set>)` is TRUE, the NULL row included.
query I rowsort
SELECT id FROM oc WHERE oc.id NOT IN (SELECT ic.id FROM ic WHERE oc.g > 0);
----
2
NULL

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

New test, and it is the one that shows what the joins.slt plan change buys. main returns no rows for this query. DuckDB 1.5.2 returns the two rows below.

The correlation oc.g > 0 names no column of ic, so it cannot become a join key and stays as a residual filter. The subquery is therefore the whole of ic for an outer row whose g > 0, and empty for every other row.

D SELECT oc.id, oc.g, (SELECT list(ic.id) FROM ic WHERE oc.g > 0) AS sub FROM oc ORDER BY oc.id;
┌───────┬───────┬─────────────┐
│  id   │   g   │     sub     │
├───────┼───────┼─────────────┤
│     1 │     1 │ [5, NULL]   │
│     2 │     0 │ NULL        │
│  NULL │     0 │ NULL        │
└───────┴───────┴─────────────┘

D SELECT id FROM oc WHERE oc.id NOT IN (SELECT ic.id FROM ic WHERE oc.g > 0);
┌───────┐
│  id   │
├───────┤
│     2 │
│  NULL │
└───────┘

main builds one null-aware anti join and keeps the residual filter on it. That executor does not apply the residual when it looks for a NULL, so it finds the NULL in ic and treats every outer row as UNKNOWN, including the two whose subquery is empty. It returns nothing.

The NULL row is kept on purpose: its subquery is empty, and NULL NOT IN (<empty set>) is TRUE.

This is the same shape as the joins.slt query below, which had no result test.

Comment on lines +602 to +618
# A grouping set above the correlated filter is a different problem. The
# grand-total row of `ROLLUP` exists for every outer row, so a miss is UNKNOWN
# here, not FALSE. The pull up moves the filter above the aggregate, which
# loses that row; the same plan gives a wrong `EXISTS` too. That is a bug in
# the pull up, https://github.com/apache/datafusion/issues/25519, and is not
# changed here: the three rows for `k = 3`, `k = 9` and `k = NULL` should be
# NULL.
query IIB
SELECT co.id, co.k, co.k IN (SELECT ci.k FROM ci WHERE ci.k = co.k GROUP BY ROLLUP(ci.k)) AS m FROM co ORDER BY k, id;
----
1 1 true
2 1 true
NULL 1 true
2 2 true
NULL 3 false
9 9 false
5 NULL false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Result changed here, and the new value is the wrong one. Please read this one before the others.

The three rows for k = 3, k = 9 and k = NULL were NULL in the previous commit, and they are FALSE again now. FALSE is what main gives. DuckDB 1.5.2 gives NULL:

D SELECT co.id, co.k, co.k IN (SELECT ci.k FROM ci WHERE ci.k = co.k GROUP BY ROLLUP(ci.k)) AS m FROM co ORDER BY k, id;
┌───────┬───────┬───────┐
│  id   │   k   │   m   │
│  ...  │  ...  │  ...  │
│  NULL │     3 │  NULL │
│     9 │     9 │  NULL │
│     5 │  NULL │  NULL │
└───────┴───────┴───────┘

The NULL came from a guard that this commit removes on purpose. The guard cleared a flag for an outer join, a union or a grouping set, which made this one query correct without fixing its cause. The cause is in the pull up: it moves the correlated filter above the aggregate, and the grand-total row of ROLLUP is then one row for the whole table instead of one row for each outer row. The same plan gives a wrong EXISTS for the same tables, which the guard never touched:

# both main and this branch
SELECT co.id, co.k, EXISTS (SELECT 1 FROM ci WHERE ci.k = co.k GROUP BY ROLLUP(ci.k)) AS e FROM co;
-- gives false for k = 3, 9 and NULL; DuckDB gives true for every row

So the guard was not a fix for the bug, only for one of its symptoms, and keeping it means this rule carries a list of plan nodes that can put a NULL back into a column. I filed the cause as #25519 and left this query at the main result until that is fixed.

Tell me if you would rather keep the guard until #25519 lands. The change is one match arm, and the tests stay green either way.

Comment on lines +657 to +660
query I
SELECT id FROM naconst_corr_t1 WHERE 3 NOT IN (SELECT id FROM naconst_corr_t2 WHERE naconst_corr_t2.g > naconst_corr_t1.g);
----
2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This query gave a planning error before, and it now gives a result. DuckDB 1.5.2 agrees with the result.

The expectation was:

query error DataFusion error: Error during planning: null_aware LeftAnti join requires equi-join keys, but the join has none

3 is a constant, so the IN equality is not an equi-join key, and the correlation g > g is a residual filter. The rule asked for a null-aware anti join that the planner cannot build, so the query failed. It now becomes the mark joins that materialize the three-valued result, which need no equi-join key.

D SELECT a.id, a.g, (SELECT list(b.id) FROM naconst_corr_t2 b WHERE b.g > a.g) AS sub FROM naconst_corr_t1 a ORDER BY a.id;
┌───────┬───────┬──────────┐
│  id   │   g   │   sub    │
├───────┼───────┼──────────┤
│     1 │     1 │ [NULL]   │
│     2 │     2 │ NULL     │
└───────┴───────┴──────────┘

D SELECT id FROM naconst_corr_t1 WHERE 3 NOT IN (SELECT id FROM naconst_corr_t2 WHERE naconst_corr_t2.g > naconst_corr_t1.g);
┌────┐
│ id │
├────┤
│  2 │
└────┘

For id = 1 the subquery gives {NULL}, so 3 NOT IN ({NULL}) is UNKNOWN and the row goes. For id = 2 the subquery is empty, so the answer is TRUE and the row stays.

Comment on lines +1991 to +1995
01)Projection: join_t1.t1_id, join_t1.t1_name, join_t1.t1_int
02)--Filter: NOT CASE WHEN __correlated_sq_2.mark THEN Boolean(true) WHEN __correlated_sq_3.mark OR CAST(join_t1.t1_id AS Int64) + Int64(12) IS NULL AND __correlated_sq_4.mark THEN Boolean(NULL) ELSE Boolean(false) END
03)----LeftMark Join: Filter: join_t1.t1_int > UInt32(0)
04)------LeftMark Join: Filter: join_t1.t1_int > UInt32(0)
05)--------LeftMark Join: CAST(join_t1.t1_id AS Int64) + Int64(12) = __correlated_sq_2.join_t2.t2_id + Int64(1) Filter: join_t1.t1_int > UInt32(0)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Only the plan changed here. The result of this query is the same before and after, and it is correct in both. I want to flag the cost, because this file tests the plan and not the result.

SELECT t1_id, t1_name, t1_int FROM join_t1 WHERE join_t1.t1_id + 12 NOT IN (SELECT join_t2.t2_id + 1 FROM join_t2 WHERE join_t1.t1_int > 0);
-- main, this branch and DuckDB 1.5.2 all give: 22 b 2

join_t2.t2_id holds no NULL in this file, so the old single anti join was correct for this data. The plan is now three joins for the same answer, and two of them are nested loop joins that carry only the outer-side predicate.

The reason is that the shape is not correct in general. The correlation join_t1.t1_int > 0 names no column of the subquery, so it stays as a residual filter, and the null-aware anti join executor ignores the residual when it decides whether a NULL makes the result UNKNOWN. Put one NULL in the subquery column and main drops every row:

CREATE TABLE jt1(t1_id INT, t1_int INT) AS VALUES (11,1),(22,2),(33,0);
CREATE TABLE jt2(t2_id INT) AS VALUES (23),(NULL);
SELECT t1_id, t1_int FROM jt1 WHERE jt1.t1_id + 12 NOT IN (SELECT jt2.t2_id + 1 FROM jt2 WHERE jt1.t1_int > 0);
-- main:        (no rows)
-- this branch: 33 0
-- DuckDB:      33 0

The row t1_id = 33 has t1_int = 0, so its correlated subquery is empty and NOT IN (<empty set>) is TRUE.

subquery_projection.slt now has this shape as a result test, with the tables oc and ic, so the gain is no longer only in this comment. #25336 is the executor gap behind it. When #25339 lands, this shape can go back to one anti join and this plan becomes small again. I can also hold this file at the old plan and let only the projected IN take the new path, if you would rather not pay three joins for a WHERE clause today.

Comment thread datafusion/optimizer/src/decorrelate.rs Outdated
.all(|&e| can_pullup_over_aggregation(e));
let (mut join_filters, subquery_filters) =
find_join_exprs(subquery_filter_exprs)?;
for expr in &join_filters {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you, this is a real regression. I took your fix in 10ccc3e. correlated_filters now keeps the outer references, and filter_rejects_null matches each side of a conjunct against the side of the join that the key is on. Your two queries are in subquery_projection.slt (tables sh_o and sh_t), and DuckDB 1.5.2 gives the same results: NULL for the NULL row, and 8 for the NOT IN filter.

// row", which is the weaker fact that this case needs, and which the first
// reading implies. `IS NULL` is two-valued on both sides, so neither test
// adds an UNKNOWN of its own.
let unknown_alias = alias.next("__correlated_sq");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I removed 50d6e4a from this PR. I also removed 1a9d842, which only fixed an error message that the merged join caused. The fallback for a residual filter is now the same plan as on main.

After the rebase onto #25339, a NOT IN filter with a residual filter no longer uses the fallback. It becomes one null-aware anti join, because that join now applies the residual filter. The nai_res_og plan that #25339 pins stays as it is, and the joins.slt plan is the same as on main again.

@adriangb
adriangb requested a review from jayzhan211 September 24, 2026 18:44

@jayzhan211 jayzhan211 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 @adriangb , here is a suggestion

.all(|&e| can_pullup_over_aggregation(e));
for expr in &subquery_filter_exprs {
if expr.contains_outer() && !self.correlated_filters.contains(expr) {
self.correlated_filters.push((*expr).clone());

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.

correlated_filters records a conjunct regardless of what sits above the Filter. A LEFT JOIN (filter on the nullable side) or a ROLLUP above it puts NULLs back into the key, but filter_rejects_null still reads u.y = o.x as "never NULL", so the NOT IN anti join loses null_aware. main returns the correct result for both queries below:

CREATE TABLE o(x INT) AS VALUES (1),(3),(NULL);
CREATE TABLE t(k INT) AS VALUES (1),(2);
CREATE TABLE u(y INT, k INT) AS VALUES (1,1);
SELECT x FROM o WHERE x NOT IN (
  SELECT u.y FROM t LEFT JOIN (SELECT * FROM u WHERE u.y = o.x) AS u ON t.k = u.k);
-- expected (and main): no rows; this PR: 3, NULL

-- co/ci from subquery_projection.slt
SELECT co.id, co.k FROM co WHERE co.k NOT IN (
  SELECT ci.k FROM ci WHERE ci.k = co.k GROUP BY ROLLUP(ci.k));
-- expected (and main): no rows; this PR: (NULL,3), (9,9), (5,NULL)

Fix: drop the recorded filters when the pull up passes such a node, and add both queries to the slt:

     fn f_up(&mut self, plan: LogicalPlan) -> Result<Transformed<LogicalPlan>> {
+        // A node that null-extends or regroups rows can put a NULL back into
+        // a column that a correlated filter below it rejected.
+        if may_reintroduce_nulls(&plan) {
+            self.correlated_filters.clear();
+        }
         let subquery_schema = plan.schema();
fn may_reintroduce_nulls(plan: &LogicalPlan) -> bool {
    match plan {
        LogicalPlan::Join(join) => matches!(
            join.join_type,
            JoinType::Left | JoinType::Right | JoinType::Full
        ),
        LogicalPlan::Union(_) => true,
        LogicalPlan::Aggregate(aggregate) => aggregate
            .group_expr
            .iter()
            .any(|e| matches!(e, Expr::GroupingSet(_))),
        _ => false,
    }
}

@adriangb
adriangb force-pushed the in-exists-subquery-projection branch from 5148cd3 to 61a61ef Compare September 28, 2026 04:12
@adriangb
adriangb requested a review from jayzhan211 September 28, 2026 04:27

@jayzhan211 jayzhan211 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 @adriangb , LGTM!

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

@adriangb,

Thanks for the update. The projected IN / NOT IN rewrite now preserves three-valued NULL semantics across the covered expression, correlation, empty-subquery, grouping-set, and name-shadowing cases. The added plan and execution coverage addresses the earlier review points.

@adriangb
adriangb enabled auto-merge September 28, 2026 15:26
@adriangb

Copy link
Copy Markdown
Contributor Author

@kosiew @jayzhan211 thanks so much for helping get this across the line!!

adriangb and others added 3 commits September 29, 2026 14:13
…ections

A projected `IN` / `NOT IN` subquery used three mark joins to materialize
its three-valued result. When the mark column of the first join is already
exact, that join alone is the full rewrite:

- no side of the `IN` predicate can be NULL in the scope of an outer row,
  or
- the join is null-aware and the `IN` equality is its first key. The
  null-aware hash join also applies a residual non-equality filter when it
  decides whether a NULL makes the mark UNKNOWN (apache#25339).

Key nullability is read from the key expressions, not only their columns,
so `NULLIF(id, 1)` or `TRY_CAST(s AS INT)` over a non-nullable column is
treated as nullable. A correlated conjunct that rejects NULL on a key, such
as a correlation that repeats the `IN` predicate, keeps that key out of the
null-aware decision (apache#25480).

A `NOT IN` filter whose `IN` equality cannot be the first key of a
null-aware anti join now falls back to the mark-join materialization
instead of planning a wrong or unsupported anti join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a NULL

A correlated filter that rejects NULL on a key only bounds the subquery
result if nothing above it can put a NULL back. An outer join, a union or a
grouping set can. With `ROLLUP`, the grand-total row is a NULL for every
outer row, so a miss is UNKNOWN, but the join was planned without
null-aware semantics and gave FALSE:

    SELECT k, k IN (SELECT i.k FROM i WHERE i.k = o.k GROUP BY ROLLUP(i.k))
    FROM o;

The pull up now clears the recorded filters when it passes such a node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The correlation on `<` now stays a residual filter of the one null-aware
mark join, so the query no longer keeps the nested-loop plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriangb
adriangb force-pushed the in-exists-subquery-projection branch from 8aa0b87 to 210d4e5 Compare September 29, 2026 19:13
@adriangb
adriangb added this pull request to the merge queue Sep 29, 2026
Merged via the queue into apache:main with commit b9c8b3d Sep 29, 2026
42 checks passed
@adriangb
adriangb deleted the in-exists-subquery-projection branch September 29, 2026 20:03
martin-g pushed a commit to martin-g/datafusion that referenced this pull request Sep 30, 2026
…ections (apache#25338)

## Which issue does this PR close?

- Closes apache#25341.

This PR supersedes apache#21363 by
@crm26. It reuses the single mark join approach from that PR, and @crm26
is a co-author of the main commit.

It also builds on apache#24972, which
added the decorrelation of `IN` subqueries in a projection.

## Rationale for this change

An `IN` or `NOT IN` subquery in a SELECT list gives correct results
today, but the plan is quadratic. The optimizer builds three mark joins
for each subquery. Two of them have no join predicate, so they run as
nested loop joins over all outer rows and all inner rows. `COALESCE`
duplicates the expression, so that shape gets six mark joins.

This PR keeps one hash mark join for each subquery. The join is
null-aware when a key can be NULL. A non-equality correlation (Q6 below)
stays a residual filter of that join, which the null-aware hash join
applies since apache#25339.

| Shape | Query | main | this PR | Result rows identical |
| --- | --- | --- | --- | --- |
| Q1 | Bare `IN` in the SELECT list | 12.893 s | 0.005 s | yes |
| Q2 | `COALESCE((x IN (...))::boolean, false)` | 36.430 s | 0.014 s |
yes |
| Q3 | Correlated `IN` with an equality predicate | 0.278 s | 0.008 s |
yes |
| Q4 | Two `IN` subqueries in separate columns | 20.904 s | 0.011 s |
yes |
| Q5 | Correlated `EXISTS` | 0.005 s | 0.003 s | yes |
| Q6 | Correlated `IN` with a non-equality predicate | 2.489 s | 0.018 s
| yes |

The table is one run of the script below after the rebase onto `main` at
`39ca2c74a9`, `datafusion-cli` built with `--profile release-nonlto` and
the default `target_partitions`. Q6 in 3 more interleaved runs: `main`
2.57 s to 3.18 s, this PR 0.022 s to 0.058 s.

These shapes are the `projection_subquery` benchmark suite from
apache#25346.

<details>
<summary>Reproduction with datafusion-cli: script, plans and timings for
each shape</summary>

Run the script with `datafusion-cli -f mre.sql`. It creates two tables
with 200000 rows each, then for each shape it prints `EXPLAIN` and runs
the query. `datafusion-cli` prints the elapsed time after each
statement. The result rows of every query are identical between the two
builds.

The plans and times for Q1 to Q5 below are from the first version of
this PR, with `main` at 22651d2 and `target_partitions = 4`. The
plans of this PR for those shapes did not change with the rebase. Q6 was
captured again after the rebase.

```sql
SET datafusion.execution.target_partitions = 4;
CREATE TABLE outer_t AS SELECT CAST(v AS INT) AS id, CAST(v % 1000 AS INT) AS z FROM (SELECT unnest(generate_series(1, 200000)) AS v);
CREATE TABLE inner_t AS SELECT CASE WHEN v % 97 = 0 THEN NULL ELSE CAST(v * 2 AS INT) END AS id, CAST(v % 1000 AS INT) AS z FROM (SELECT unnest(generate_series(1, 200000)) AS v);
SELECT 'Q1' AS shape;
EXPLAIN SELECT id, id IN (SELECT id FROM inner_t) AS m FROM outer_t;
SELECT count(*) FILTER (WHERE m), count(*) FILTER (WHERE m IS NULL) FROM (SELECT id, id IN (SELECT id FROM inner_t) AS m FROM outer_t);
SELECT 'Q2' AS shape;
EXPLAIN SELECT id, COALESCE((id IN (SELECT id FROM inner_t))::boolean, false) AS m FROM outer_t;
SELECT count(*) FILTER (WHERE m) FROM (SELECT id, COALESCE((id IN (SELECT id FROM inner_t))::boolean, false) AS m FROM outer_t);
SELECT 'Q3' AS shape;
EXPLAIN SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z = o.z) AS m FROM outer_t o;
SELECT count(*) FILTER (WHERE m), count(*) FILTER (WHERE m IS NULL) FROM (SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z = o.z) AS m FROM outer_t o);
SELECT 'Q4' AS shape;
EXPLAIN SELECT id IN (SELECT id FROM inner_t) AS a, id IN (SELECT id FROM inner_t WHERE z < 500) AS b FROM outer_t;
SELECT count(*) FILTER (WHERE a), count(*) FILTER (WHERE b) FROM (SELECT id IN (SELECT id FROM inner_t) AS a, id IN (SELECT id FROM inner_t WHERE z < 500) AS b FROM outer_t);
SELECT 'Q5' AS shape;
EXPLAIN SELECT id, EXISTS (SELECT 1 FROM inner_t i WHERE i.id = o.id) AS e FROM outer_t o;
SELECT count(*) FILTER (WHERE e) FROM (SELECT id, EXISTS (SELECT 1 FROM inner_t i WHERE i.id = o.id) AS e FROM outer_t o);
SELECT 'Q6' AS shape;
EXPLAIN SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z < o.z) AS m FROM outer_t o;
SELECT count(*) FILTER (WHERE m), count(*) FILTER (WHERE m IS NULL) FROM (SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z < o.z) AS m FROM outer_t o);
```

<details>
<summary>Q1: Bare `IN` in the SELECT list, plans on main and on this
PR</summary>

**main**, query time 190.730 s

```
logical_plan
Projection: outer_t.id, __correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR outer_t.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS m
  LeftMark Join:
    LeftMark Join:
      LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
        TableScan: outer_t projection=[id]
        SubqueryAlias: __correlated_sq_1
          TableScan: inner_t projection=[id]
      SubqueryAlias: __correlated_sq_2
        Filter: inner_t.id IS NULL
          TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_3
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL as m]
  NestedLoopJoinExec: join_type=LeftMark
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark
        CoalescePartitionsExec
          FilterExec: id@0 IS NULL
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
          CoalescePartitionsExec
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

**this PR**, query time 0.034 s

```
logical_plan
Projection: outer_t.id, __correlated_sq_1.mark AS m
  LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
    TableScan: outer_t projection=[id]
    SubqueryAlias: __correlated_sq_1
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

</details>

<details>
<summary>Q2: `COALESCE((x IN (...))::boolean, false)`, plans on main and
on this PR</summary>

**main**, query time 351.579 s

```
logical_plan
Projection: outer_t.id, CAST(__correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR outer_t.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS Boolean) IS NOT NULL AND CAST(__correlated_sq_4.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_5.mark OR outer_t.id IS NULL AND __correlated_sq_6.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_4.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS Boolean) AS m
  LeftMark Join:
    LeftMark Join:
      LeftMark Join: outer_t.id = __correlated_sq_4.id null_aware
        LeftMark Join:
          LeftMark Join:
            LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
              TableScan: outer_t projection=[id]
              SubqueryAlias: __correlated_sq_1
                TableScan: inner_t projection=[id]
            SubqueryAlias: __correlated_sq_2
              Filter: inner_t.id IS NULL
                TableScan: inner_t projection=[id]
          SubqueryAlias: __correlated_sq_3
            TableScan: inner_t projection=[id]
        SubqueryAlias: __correlated_sq_4
          TableScan: inner_t projection=[id]
      SubqueryAlias: __correlated_sq_5
        Filter: inner_t.id IS NULL
          TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_6
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL IS NOT NULL AND (mark@4 IS NOT DISTINCT FROM true OR (mark@5 OR id@0 IS NULL AND mark@6) IS NOT DISTINCT FROM true AND mark@4 IS DISTINCT FROM true AND NULL) as m]
  NestedLoopJoinExec: join_type=LeftMark
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark
        CoalescePartitionsExec
          FilterExec: id@0 IS NULL
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
          CoalescePartitionsExec
            NestedLoopJoinExec: join_type=LeftMark
              CoalescePartitionsExec
                NestedLoopJoinExec: join_type=RightMark
                  CoalescePartitionsExec
                    FilterExec: id@0 IS NULL
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
                    CoalescePartitionsExec
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
              DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

**this PR**, query time 0.054 s

```
logical_plan
Projection: outer_t.id, CAST(__correlated_sq_1.mark AS Boolean) IS NOT NULL AND CAST(__correlated_sq_2.mark AS Boolean) AS m
  LeftMark Join: outer_t.id = __correlated_sq_2.id null_aware
    LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
      TableScan: outer_t projection=[id]
      SubqueryAlias: __correlated_sq_1
        TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_2
      TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT NULL AND mark@2 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
    CoalescePartitionsExec
      HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
        CoalescePartitionsExec
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

</details>

<details>
<summary>Q3: Correlated `IN` with an equality predicate, plans on main
and on this PR</summary>

**main**, query time 1.372 s

```
logical_plan
Projection: o.id, __correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR o.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS m
  LeftMark Join: o.z = __correlated_sq_3.z
    LeftMark Join: o.z = __correlated_sq_2.z
      LeftMark Join: o.id = __correlated_sq_1.id, o.z = __correlated_sq_1.z null_aware
        SubqueryAlias: o
          TableScan: outer_t projection=[id, z]
        SubqueryAlias: __correlated_sq_1
          SubqueryAlias: i
            TableScan: inner_t projection=[id, z]
      SubqueryAlias: __correlated_sq_2
        SubqueryAlias: i
          Projection: inner_t.z
            Filter: inner_t.id IS NULL
              TableScan: inner_t projection=[id, z]
    SubqueryAlias: __correlated_sq_3
      SubqueryAlias: i
        TableScan: inner_t projection=[z]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL as m]
  HashJoinExec: mode=CollectLeft, join_type=RightMark, on=[(z@0, z@1)], projection=[id@0, mark@2, mark@3, mark@4]
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    HashJoinExec: mode=CollectLeft, join_type=RightMark, on=[(z@0, z@1)]
      CoalescePartitionsExec
        FilterExec: id@0 IS NULL, projection=[z@1]
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
      HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0), (z@1, z@1)], null_aware
        CoalescePartitionsExec
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

**this PR**, query time 0.091 s

```
logical_plan
Projection: o.id, __correlated_sq_1.mark AS m
  LeftMark Join: o.id = __correlated_sq_1.id, o.z = __correlated_sq_1.z null_aware
    SubqueryAlias: o
      TableScan: outer_t projection=[id, z]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0), (z@1, z@1)], projection=[id@0, mark@2], null_aware
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

</details>

<details>
<summary>Q4: Two `IN` subqueries in separate columns, plans on main and
on this PR</summary>

**main**, query time 296.676 s

```
logical_plan
Projection: __correlated_sq_1.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_2.mark OR outer_t.id IS NULL AND __correlated_sq_3.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_1.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS a, __correlated_sq_4.mark IS NOT DISTINCT FROM Boolean(true) OR (__correlated_sq_5.mark OR outer_t.id IS NULL AND __correlated_sq_6.mark) IS NOT DISTINCT FROM Boolean(true) AND __correlated_sq_4.mark IS DISTINCT FROM Boolean(true) AND Boolean(NULL) AS b
  LeftMark Join:
    LeftMark Join:
      LeftMark Join: outer_t.id = __correlated_sq_4.id null_aware
        LeftMark Join:
          LeftMark Join:
            LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
              TableScan: outer_t projection=[id]
              SubqueryAlias: __correlated_sq_1
                TableScan: inner_t projection=[id]
            SubqueryAlias: __correlated_sq_2
              Filter: inner_t.id IS NULL
                TableScan: inner_t projection=[id]
          SubqueryAlias: __correlated_sq_3
            TableScan: inner_t projection=[id]
        SubqueryAlias: __correlated_sq_4
          Projection: inner_t.id
            Filter: inner_t.z < Int32(500)
              TableScan: inner_t projection=[id, z]
      SubqueryAlias: __correlated_sq_5
        Projection: inner_t.id
          Filter: inner_t.z < Int32(500) AND inner_t.id IS NULL
            TableScan: inner_t projection=[id, z]
    SubqueryAlias: __correlated_sq_6
      Projection: inner_t.id
        Filter: inner_t.z < Int32(500)
          TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[mark@1 IS NOT DISTINCT FROM true OR (mark@2 OR id@0 IS NULL AND mark@3) IS NOT DISTINCT FROM true AND mark@1 IS DISTINCT FROM true AND NULL as a, mark@4 IS NOT DISTINCT FROM true OR (mark@5 OR id@0 IS NULL AND mark@6) IS NOT DISTINCT FROM true AND mark@4 IS DISTINCT FROM true AND NULL as b]
  NestedLoopJoinExec: join_type=LeftMark
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark
        CoalescePartitionsExec
          FilterExec: z@1 < 500 AND id@0 IS NULL, projection=[id@0]
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
          CoalescePartitionsExec
            NestedLoopJoinExec: join_type=LeftMark
              CoalescePartitionsExec
                NestedLoopJoinExec: join_type=RightMark
                  CoalescePartitionsExec
                    FilterExec: id@0 IS NULL
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
                    CoalescePartitionsExec
                      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
                    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
              DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
          FilterExec: z@1 < 500, projection=[id@0]
            DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    FilterExec: z@1 < 500, projection=[id@0]
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

**this PR**, query time 0.063 s

```
logical_plan
Projection: __correlated_sq_1.mark AS a, __correlated_sq_2.mark AS b
  LeftMark Join: outer_t.id = __correlated_sq_2.id null_aware
    LeftMark Join: outer_t.id = __correlated_sq_1.id null_aware
      TableScan: outer_t projection=[id]
      SubqueryAlias: __correlated_sq_1
        TableScan: inner_t projection=[id]
    SubqueryAlias: __correlated_sq_2
      Projection: inner_t.id
        Filter: inner_t.z < Int32(500)
          TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[mark@0 as a, mark@1 as b]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], projection=[mark@1, mark@2], null_aware
    CoalescePartitionsExec
      HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], null_aware
        CoalescePartitionsExec
          DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
        DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    FilterExec: z@1 < 500, projection=[id@0]
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

</details>

<details>
<summary>Q5: Correlated `EXISTS`, plans on main and on this PR</summary>

**main**, query time 0.021 s

```
logical_plan
Projection: o.id, __correlated_sq_1.mark AS e
  LeftMark Join: o.id = __correlated_sq_1.id
    SubqueryAlias: o
      TableScan: outer_t projection=[id]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as e]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)]
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

**this PR**, query time 0.021 s

```
logical_plan
Projection: o.id, __correlated_sq_1.mark AS e
  LeftMark Join: o.id = __correlated_sq_1.id
    SubqueryAlias: o
      TableScan: outer_t projection=[id]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as e]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)]
    CoalescePartitionsExec
      DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]
    DataSourceExec: partitions=4, partition_sizes=[7, 6, 6, 6]

```

</details>

<details>
<summary>Q6: Correlated `IN` with a non-equality predicate, plans on
main and on this PR</summary>

**main** (`39ca2c74a9`), query time 2.489 s

```
logical_plan
Projection: o.id, CASE WHEN __correlated_sq_1.mark THEN Boolean(true) WHEN __correlated_sq_2.mark OR o.id IS NULL AND __correlated_sq_3.mark THEN Boolean(NULL) ELSE Boolean(false) END AS m
  LeftMark Join:  Filter: __correlated_sq_3.z < o.z
    LeftMark Join:  Filter: __correlated_sq_2.z < o.z
      LeftMark Join: o.id = __correlated_sq_1.id Filter: __correlated_sq_1.z < o.z null_aware
        SubqueryAlias: o
          TableScan: outer_t projection=[id, z]
        SubqueryAlias: __correlated_sq_1
          SubqueryAlias: i
            TableScan: inner_t projection=[id, z]
      SubqueryAlias: __correlated_sq_2
        SubqueryAlias: i
          Projection: inner_t.z
            Filter: inner_t.id IS NULL
              TableScan: inner_t projection=[id, z]
    SubqueryAlias: __correlated_sq_3
      SubqueryAlias: i
        TableScan: inner_t projection=[z]
physical_plan
ProjectionExec: expr=[id@0 as id, CASE WHEN mark@1 THEN true WHEN mark@2 OR id@0 IS NULL AND mark@3 THEN NULL ELSE false END as m]
  NestedLoopJoinExec: join_type=LeftMark, filter=z@1 < z@0, projection=[id@0, mark@2, mark@3, mark@4]
    CoalescePartitionsExec
      NestedLoopJoinExec: join_type=RightMark, filter=z@1 < z@0
        CoalescePartitionsExec
          FilterExec: id@0 IS NULL, projection=[z@1]
            DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
        HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], filter=z@1 < z@0, null_aware
          CoalescePartitionsExec
            DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
          DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
    DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
```

**this PR**, query time 0.018 s

```
logical_plan
Projection: o.id, __correlated_sq_1.mark AS m
  LeftMark Join: o.id = __correlated_sq_1.id Filter: __correlated_sq_1.z < o.z null_aware
    SubqueryAlias: o
      TableScan: outer_t projection=[id, z]
    SubqueryAlias: __correlated_sq_1
      SubqueryAlias: i
        TableScan: inner_t projection=[id, z]
physical_plan
ProjectionExec: expr=[id@0 as id, mark@1 as m]
  HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(id@0, id@0)], filter=z@1 < z@0, projection=[id@0, mark@2], null_aware
    CoalescePartitionsExec
      DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
    DataSourceExec: partitions=12, partition_sizes=[3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
```

</details>

</details>

## What changes are included in this PR?

1. `build_join` now reports whether the mark column of the `LeftMark`
join is exact under three-valued logic. It is exact when no side of the
`IN` predicate can be NULL in scope, or when the join is null-aware and
the `IN` equality is its first key. A residual non-equality filter does
not change this, because the null-aware hash join applies it when it
decides whether a NULL makes the mark UNKNOWN
(apache#25339).
`in_subquery_value_mark_join` builds this join first. If the mark is
exact, it returns the mark column alone, or `NOT mark` for `NOT IN`. The
three join materialization stays for the other cases.
2. The rule decides null-awareness from one question: can the `IN` value
or the subquery output be NULL inside the scope of an outer row? Only
then can `IN` be UNKNOWN.
- The test uses the key expressions, not the columns they reference. A
key such as `NULLIF(id, 1)` or `TRY_CAST(s AS INT)` can be NULL over a
`NOT NULL` column.
- `PullUpCorrelatedExpr` now records every correlated conjunct, before
it drops the conjunct that repeats the `IN` predicate. A conjunct such
as `y = x`, `y > x` or `y IS NOT NULL` is never TRUE for a NULL `y`, so
it keeps a NULL `y` out of the scope. The conjuncts keep their outer
references, so a subquery column with the same qualified name as an
outer column is not mistaken for the outer value.
- A correlation key that is NULL only empties the scope, so it never
makes the join null-aware.
- The recorded conjuncts are cleared when the pull up passes an outer
join, a union or a grouping set, because such a node can put a NULL back
into the column. With `ROLLUP`, the grand-total row is a NULL for every
outer row.
3. A `NOT IN` filter uses the same scope-aware nullability for its
null-aware `LeftAnti` join. If the `IN` equality does not become the
first key of that join, the rule gives up and the `NOT IN` becomes the
mark joins, which give the correct result.
4. The regression test for
apache#24574 in
`projection_pushdown.slt` now uses a correlated subquery with `LIMIT 1`.
The subquery then still reaches `ExtractLeafExpressions`, and the alias
generator still starts at 2 inside it. The earlier version of the test
let the subquery be flattened, so it no longer exercised the scan inside
a subquery path.
5. New sqllogictest cases in `subquery_projection.slt`:
- `EXPLAIN` guards: one hash mark join for each subquery, one null-aware
mark join with a residual filter, a null-aware `LeftAnti` join with two
keys, and with one key and a residual filter.
- NULL semantics: `IN` and `NOT IN` with a NULL in the subquery, a NULL
outer value, `NOT` inside `CASE`, a projection over an aggregate, the
`COALESCE` and cast shape, and the residual filter shape.
- Nullable key expressions over `NOT NULL` columns: a `NULLIF` value, a
`TRY_CAST` value and a `NULLIF` subquery output, in a projection and in
a `WHERE` clause.
- A correlation that repeats the `IN` predicate, and a subquery column
with the same qualified name as the outer value.
- The same correlation below a `ROLLUP`, as a projected `IN` with an
`EXPLAIN` guard and as a `NOT IN` filter.

The expected results agree with DuckDB 1.5.2, and the earlier cases also
with PostgreSQL.
6. The `projection_subquery` benchmark docs no longer describe q07 as
the nested-loop control.

## DuckDB comparison

For an absolute reference, the same seven queries in `datafusion-cli`
and in DuckDB 1.5.2, on the same two tables, release build, Apple M4
Pro. `main` is the merge-base `64871d9` and "this PR" is `3af87370c1`.
The later commits of this PR change the correlated and `NOT IN` filter
paths, and, after the rebase onto
apache#25339, the plan of q07. q07 is
measured again below. The tables are the ones the suite builds, at its
default of 30,000 rows each. Each number is the median of 7 runs. One
round runs one session per engine, and the rounds interleave the three
engines.

| Query | `main` | this PR | DuckDB |
| --- | --- | --- | --- |
| q01 bare `IN` | 533 ms | 2 ms | 2 ms |
| q02 `COALESCE` over `IN` | 1352 ms | 3 ms | 2 ms |
| q03 correlated `IN`, equality | 14 ms | 3 ms | 3 ms |
| q04 two `IN` columns | 1026 ms | 3 ms | 2 ms |
| q05 bare `NOT IN` | 646 ms | 1 ms | 2 ms |
| q06 correlated `EXISTS` | 4 ms | 2 ms | 2 ms |
| q07 correlated `IN`, non-equality | 117 ms | 93 ms | 458 ms |

All three engines give the same result for all seven queries, and the
counts are the ones in the suite's checked-in result files.

For the four quadratic shapes, q01, q02, q04 and q05, `main` is 270x to
680x slower than DuckDB. This PR puts them at DuckDB's cost. These times
are at the 1 ms resolution of both command line tools, so the exact
factor is approximate; the size of the difference is not.

q07 now uses one null-aware mark join with the `<` correlation as a
residual filter. Measured again after the rebase, median of 7 runs at
30,000 rows: `main` at `39ca2c74a9` 78 ms, this PR 3 ms, DuckDB 404 ms.
The counts are the same in all three.

A second run at 100,000 rows per table gives the same picture: `main`
takes 2741 ms to 6355 ms for q01, q02, q04 and q05, this PR takes 2 ms
to 4 ms and DuckDB 3 ms to 4 ms. All three engines again give the same
counts.

<details><summary>How to run it</summary>

The queries are the suite's own
`benchmarks/sql_benchmarks/projection_subquery/queries/q0*.sql`, with an
alias added to the derived table. The load SQL needs one change for
DuckDB, because `generate_series` gives a column of that name there and
a column named `value` in DataFusion:

```sql
CREATE TABLE outer_t AS
SELECT
  CAST(value AS INT) AS id,
  CAST(value % 1000 AS INT) AS z
FROM generate_series(1, 30000) AS g(value);

CREATE TABLE inner_t AS
SELECT
  CASE WHEN value % 97 = 0 THEN NULL ELSE CAST(value * 2 AS INT) END AS id,
  CAST(value % 1000 AS INT) AS z
FROM generate_series(1, 30000) AS g(value);
```

`datafusion-cli` reports the time of each statement as `Elapsed`, and
`duckdb` reports it as `Run Time (s): real` after `.timer on`.

</details>

## What is the testing strategy for this PR?

- Unit tests in
`datafusion/optimizer/src/decorrelate_predicate_subquery.rs`. The
updated snapshots show a single mark join. New tests cover a residual
filter, `NOT IN`, nullable key expressions, a correlation that repeats
the `IN` predicate, and the same correlation below a `ROLLUP`. The last
one fails without the clearing in item 2.
- The new sqllogictest cases in `subquery_projection.slt` listed above.
- The full sqllogictest suite, the optimizer crate and workspace clippy
are green locally.
- The benchmark numbers above.

## Are there any user-facing changes?

Plans for projected `IN` and `NOT IN` subqueries are much faster. There
are no changes to any public API, and no documentation change is needed.

Some query results change, and each change is a correction. DuckDB 1.5.2
gives the new results:

- An `IN` or `NOT IN` whose key is an expression that can be NULL over a
`NOT NULL` column, such as `NULLIF(id, 1)`. For a projected `IN`, `main`
returns `false` instead of `NULL`. For a `NOT IN` filter, `main` keeps
rows that it must drop. The correlated form of that is
apache#25347.
- A correlated `NOT IN` whose correlation repeats the `IN` predicate, `x
NOT IN (SELECT y FROM t WHERE y = x)`. `main` drops rows that it must
keep (apache#25480).
- A projected `IN` whose correlation repeats the `IN` predicate below a
`ROLLUP`, `k IN (SELECT i.k FROM i WHERE i.k = o.k GROUP BY
ROLLUP(i.k))`. `main` fails to plan it since
apache#25529. It now gives NULL for a
miss, as DuckDB does.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
mohitgurav20 pushed a commit to mohitgurav20/datafusion that referenced this pull request Oct 6, 2026
## Which issue does this PR close?

Closes apache#25473.  
Closes apache#25474.

## Rationale for this change

Issues apache#25473 and apache#25474 reported wrong results for `NOT IN (subquery)`
when the subquery column is `NOT NULL` but the value being compared can
evaluate to `NULL`.

This includes NULL constants such as `CAST(NULL AS INT)` and nullable
expressions over `NOT NULL` columns, such as a `CASE` expression without
an `ELSE`.

The fix itself landed in apache#25338, which determines null-awareness from
the full `IN` operand expressions. This PR adds the missing regression
coverage for both issues so this behavior does not regress.

## What changes are included in this PR?

This is a test-only change. There are no production code or public API
changes.

It adds optimizer unit tests that verify:

- A NULL constant compared against a non-nullable subquery column
produces a projected `__correlated_sq_1_value` and a `null_aware`
`LeftAnti` join.
- A non-nullable expression such as `test.c + 1`, where `test.c` is
non-nullable, continues to use a regular `LeftAnti` join without
`null_aware`.

It adds SQL logic tests for null-aware anti joins covering:

- `CAST(NULL AS INT)`, untyped `NULL`, and `NULLIF(1, 1)`.
- Nullable expressions over `NOT NULL` columns.
- Nullable expressions on the subquery side.
- NULL values against an empty subquery.
- A non-NULL constant control.
- A non-nullable `x + 1` control.
- Logical and physical `EXPLAIN` plans for the NULL constant, nullable
`CASE`, and non-nullable `x + 1` cases.
- The NULL constant and nullable `CASE` cases with
`datafusion.execution.target_partitions = 1`.

It also adds SQL logic tests for the mark-join path, including the `...
NOT IN (...) OR x = 99` form and a `SELECT`-list control that verifies
the mark evaluates to `NULL`.

## Are these changes tested?

Yes. This PR adds regression tests in:

- `datafusion/optimizer/src/decorrelate_predicate_subquery.rs`
- `datafusion/sqllogictest/test_files/null_aware_anti_join.slt`
- `datafusion/sqllogictest/test_files/null_aware_mark_join.slt`

The new tests verify both result correctness and expected
logical/physical plan shapes, including that nullable operands use
null-aware joins while non-nullable operands remain non-null-aware.

Verification:

```bash
cargo test -p datafusion-optimizer decorrelate_predicate_subquery
cargo test -p datafusion-sqllogictest --test sqllogictests -- null_aware
cargo test -p datafusion-sqllogictest --test sqllogictests -- subquery_projection
```

## Are there any user-facing changes?

No new user-facing behavior is introduced by this PR. The underlying
correctness fix already landed in apache#25338.

This PR only adds regression coverage for that behavior and does not
change any public APIs.

## LLM-generated code disclosure

This PR includes LLM-generated code and comments. All LLM-generated
content has been manually reviewed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt) v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Projected IN subqueries plan two extra nested-loop mark joins per subquery and become quadratic

7 participants