Skip to content

Allow a non-distinct count alongside the single distinct aggregate in SingleDistinctToGroupBy - #24859

Draft
adriangb wants to merge 2 commits into
mainfrom
claude/single-distinct-to-groupby-allow-count
Draft

Allow a non-distinct count alongside the single distinct aggregate in SingleDistinctToGroupBy#24859
adriangb wants to merge 2 commits into
mainfrom
claude/single-distinct-to-groupby-allow-count

Conversation

@adriangb

@adriangb adriangb commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

No existing issue. This was found while investigating a production out of memory. Happy to file one if you would like it tracked for the changelog.

Rationale for this change

SingleDistinctToGroupBy rewrites AGG(DISTINCT x) into a two phase group by, which keeps a high cardinality distinct off the one-boxed-accumulator-per-group path in GroupsAccumulatorAdapter. The rule already tolerates a non-distinct sum, min or max next to the distinct aggregate, but bails out on count, so the very common

SELECT g, count(*), count(DISTINCT x) FROM t GROUP BY g

shape keeps the unrewritten plan and its memory profile.

We hit this in production: a query of exactly that shape drove a process running DataFusion to 10.98 GB and death, and the single count(*) was the only reason the rewrite did not apply.

What changes are included in this PR?

This allows a non-distinct count as well. count is the one supported function whose outer phase is a different function: the inner group by counts the rows of each (group, distinct value) partition, and the outer phase adds those partial counts up with sum, since count over a group is the sum of the counts of any partition of that group.

Two details follow from the substitution:

  • count and sum come from the session function registry (as replace_distinct_aggregate already does for first_value), and the rewrite only fires when the aggregate is that exact count, compared by identity rather than by name. A session without a registry, or with its own count, is left alone.
  • count returns a non-null 0 over an empty input while sum of no rows is NULL, which is reachable for an aggregate with no GROUP BY: the inner aggregate emits no rows and the outer still emits one, so SELECT count(*), count(DISTINCT x) FROM empty would return NULL, 0 instead of 0, 0. The projection selects CASE WHEN sum(alias) IS NOT NULL THEN sum(alias) ELSE 0 END, restoring the 0 and keeping the column's type and nullability as count had them.

FILTER and ORDER BY still block the rewrite.

Files touched beyond the rule itself:

  • datafusion/sqllogictest/test_files/single_distinct_to_groupby.slt: the new coverage described below.
  • datafusion/sqllogictest/test_files/clickbench.slt: the one existing snapshot in the repository that changes, discussed below.
  • datafusion/substrait/tests/cases/roundtrip_logical_plan.rs: aggregate_distinct_with_having now builds its session with this rule removed, so it keeps round tripping the plan shape the test was written for.

What is the testing strategy for this PR?

single_distinct_to_groupby.slt asserts every result twice, once under datafusion.optimizer.max_passes = 0 and once under the default, with identical expected blocks, so a null-handling or type error surfaces as a result mismatch rather than only a plan diff. It covers count(*) vs count(1) vs count(col) grouped and ungrouped, a group whose distinct column is entirely NULL, a group with NULLs in both the distinct and summed columns, empty input three ways, HAVING plus ORDER BY on the rewritten count, and the production join shape.

Exactly one existing snapshot in the repository changes: the ClickBench Q22 EXPLAIN, which is this shape verbatim. Its result block directly beneath, running on real ClickBench parquet, is unchanged. The physical SortExec: TopK(fetch=10) moves from below the projection to above it, because the sort key is now a CASE output rather than a raw aggregate column. That is order-equivalent, since the CASE is the identity on every non-NULL input and the sum is never NULL in a grouped aggregate.

Run locally: the full sqllogictest suite, plus datafusion --test core_integration (1079), --test tpcds_planning (198) and -p datafusion --lib (444). cargo clippy -p datafusion-optimizer --all-targets is clean.

Benchmarks

Q22 is the only ClickBench query whose plan changes. Q9 (RegionID, SUM, COUNT(*), AVG, COUNT(DISTINCT UserID)) still bails out, because AVG disqualifies it.

Measured on clickbench_partitioned (100 files, ~100M rows), release builds of this branch and of the base commit it sat on at the time of the run, on a 12-core machine.

A run-level A/B could not resolve a change this small here. Comparing the base binary against itself with compare.py reported 9 queries faster, 28 slower and 6 unchanged, with swings up to 1.58x, and two runs of the same base-vs-branch comparison gave opposite verdicts (11 faster / 21 slower, then 30 faster / 6 slower, with a 1.97x swing). Those tables measure background load, not the patch, because one arm is a full 43-query pass of about four minutes and load drifts between the arms.

Instead the arms were paired per query, running base and branch back to back and alternating which goes first, over 40 repetitions. The 42 queries whose plans are unchanged then serve as an in-experiment control for residual bias.

Q22, net of the control bias (difference in differences, bootstrap CI):

-2.03%   95% CI [-5.48%, +1.39%]

The interval includes zero, so there is no measurable latency difference, and the 95% upper bound excludes a Q22 regression larger than about 1.5%. Pooled controls moved +0.36% [-0.64%, +1.05%], confirming the setup resolves effects of roughly 3% and no better.

This is latency-neutral on ClickBench, consistent with #11360, which found removing the rule entirely to be a wash. The case for the change rests on the memory behaviour of the rewritten plan, not on latency.

Not covered: memory. These runs used no --memory-limit, so they do not exercise the spilling behaviour that motivates the rewrite. That is a separate experiment.

Are there any user-facing changes?

No public API change and no change to query results. Plans for SELECT ..., count(...), count(DISTINCT x) ... GROUP BY ... change shape, so EXPLAIN output for that shape differs, and such queries should use substantially less memory. The ClickBench Q22 plan change above is the visible example.

adriangb and others added 2 commits September 1, 2026 12:53
`SingleDistinctToGroupBy` rewrites `AGG(DISTINCT x)` into a two phase
group by, which is what keeps a high cardinality distinct off the
one-accumulator-per-group path in `GroupsAccumulatorAdapter`. The rule
tolerated a non-distinct `sum`, `min` or `max` next to the distinct
aggregate but bailed out on `count`, so the very common
`count(*), count(DISTINCT x) ... GROUP BY` shape kept the unrewritten
plan and its memory profile.

Allow a non-distinct `count` as well. `count` is the one supported
function whose outer phase is a different function: the inner group by
counts the rows of each `(group, distinct value)` partition and the
outer phase adds those partial counts up with `sum`, since count over a
group is the sum of the counts of any partition of that group.

Two details follow from that substitution:

- `count` and `sum` are resolved from the session function registry and
  the rewrite only fires when the aggregate is that exact `count`, so a
  session without a registry or with its own `count` is left alone.
- `count` returns a non-null 0 over an empty input while `sum` of no
  rows is NULL, which is reachable for an aggregate with no group by.
  The projection selects
  `CASE WHEN sum(alias) IS NOT NULL THEN sum(alias) ELSE 0 END`, which
  restores the 0 and keeps the column's type and nullability as `count`
  had them.

The new sqllogictest file asserts every result twice, once with the
optimizer disabled and once with it enabled, over data with NULL and
all-NULL distinct values, an empty input, and `count(*)` versus
`count(col)` versus `count(1)`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`aggregate_distinct_with_having` round trips
`SELECT a, count(distinct b) ... HAVING count(b) > 100` through substrait and
asserts the plan comes back displaying identically. It passed only because the
non-distinct `count` made `SingleDistinctToGroupBy` bail out, so the plan had no
aliases in it. With the rule now allowing that `count`, the query is rewritten
and the assertion fails.

The failure is a pre-existing substrait gap rather than anything specific to
this query: substrait carries no names for an aggregate's grouping and measure
expressions, so the consumer derives them from the expressions themselves and
the `alias1` and `alias2` names the rule introduces are lost. Any plan the rule
rewrites fails the same way, including the plain
`SELECT a, count(distinct b) FROM data GROUP BY a, c` that this change does not
touch.

Remove the rule from the session used by this one test, so it keeps covering the
un-rewritten aggregate it was written for instead of depending on the rule
bailing out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt) substrait Changes to the substrait crate labels Sep 1, 2026
@codecov-commenter

codecov-commenter commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.69945% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.60%. Comparing base (da89c7c) to head (f47c045).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...fusion/optimizer/src/single_distinct_to_groupby.rs 84.69% 9 Missing and 19 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #24859    +/-   ##
========================================
  Coverage   81.60%   81.60%            
========================================
  Files        1123     1123            
  Lines      408898   409051   +153     
  Branches   408898   409051   +153     
========================================
+ Hits       333670   333810   +140     
+ Misses      55625    55624     -1     
- Partials    19603    19617    +14     

☔ 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

adriangb commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

run benchmark clickbench_partitioned
env:
DATAFUSION_RUNTIME_MEMORY_LIMIT: 4G

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5500225568-2072-x9l4r 6.12.94+ #1 SMP Fri Jul 17 09:42:57 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 claude/single-distinct-to-groupby-allow-count (f47c045) to da89c7c (merge-base) diff

Run configuration
run benchmark clickbench_partitioned
env:
  DATAFUSION_RUNTIME_MEMORY_LIMIT: "4G"

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 claude/single-distinct-to-groupby-allow-count (f47c045) to da89c7c (merge-base) diff

Run configuration
run benchmark clickbench_partitioned
env:
  DATAFUSION_RUNTIME_MEMORY_LIMIT: "4G"
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 claude_single-distinct-to-groupby-allow-count
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃       HEAD ┃ claude_single-distinct-to-groupby-allow-count ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0  │    1.38 ms │                                       1.22 ms │ +1.12x faster │
│ QQuery 1  │   12.34 ms │                                      11.86 ms │     no change │
│ QQuery 2  │   37.35 ms │                                      36.73 ms │     no change │
│ QQuery 3  │   33.25 ms │                                      31.14 ms │ +1.07x faster │
│ QQuery 4  │  265.05 ms │                                     221.44 ms │ +1.20x faster │
│ QQuery 5  │  276.49 ms │                                     269.41 ms │     no change │
│ QQuery 6  │    1.28 ms │                                       1.28 ms │     no change │
│ QQuery 7  │   13.28 ms │                                      13.09 ms │     no change │
│ QQuery 8  │  337.55 ms │                                     330.68 ms │     no change │
│ QQuery 9  │  452.80 ms │                                     447.81 ms │     no change │
│ QQuery 10 │   69.67 ms │                                      69.38 ms │     no change │
│ QQuery 11 │   80.87 ms │                                      80.40 ms │     no change │
│ QQuery 12 │  265.72 ms │                                     266.16 ms │     no change │
│ QQuery 13 │  978.95 ms │                                     959.23 ms │     no change │
│ QQuery 14 │  281.40 ms │                                     287.72 ms │     no change │
│ QQuery 15 │  266.83 ms │                                     260.92 ms │     no change │
│ QQuery 16 │ 1228.08 ms │                                    1196.31 ms │     no change │
│ QQuery 17 │  916.26 ms │                                     890.67 ms │     no change │
│ QQuery 18 │ 2497.04 ms │                                    2464.68 ms │     no change │
│ QQuery 19 │   28.04 ms │                                      30.09 ms │  1.07x slower │
│ QQuery 20 │  528.22 ms │                                     524.86 ms │     no change │
│ QQuery 21 │  516.39 ms │                                     512.66 ms │     no change │
│ QQuery 22 │  980.18 ms │                                     974.68 ms │     no change │
│ QQuery 23 │ 3061.15 ms │                                    3010.47 ms │     no change │
│ QQuery 24 │   42.00 ms │                                      41.44 ms │     no change │
│ QQuery 25 │  110.73 ms │                                     109.66 ms │     no change │
│ QQuery 26 │   42.25 ms │                                      41.12 ms │     no change │
│ QQuery 27 │  511.60 ms │                                     509.48 ms │     no change │
│ QQuery 28 │ 2913.69 ms │                                    2885.57 ms │     no change │
│ QQuery 29 │   41.01 ms │                                      41.29 ms │     no change │
│ QQuery 30 │  298.78 ms │                                     296.46 ms │     no change │
│ QQuery 31 │  273.81 ms │                                     284.70 ms │     no change │
│ QQuery 32 │ 3254.62 ms │                                    3309.85 ms │     no change │
│ QQuery 33 │ 2515.41 ms │                                    2534.95 ms │     no change │
│ QQuery 34 │ 2659.40 ms │                                    2562.43 ms │     no change │
│ QQuery 35 │  280.03 ms │                                     278.34 ms │     no change │
│ QQuery 36 │   65.80 ms │                                      65.81 ms │     no change │
│ QQuery 37 │   35.17 ms │                                      35.47 ms │     no change │
│ QQuery 38 │   39.97 ms │                                      40.54 ms │     no change │
│ QQuery 39 │  133.36 ms │                                     130.60 ms │     no change │
│ QQuery 40 │   13.78 ms │                                      13.90 ms │     no change │
│ QQuery 41 │   13.60 ms │                                      13.63 ms │     no change │
│ QQuery 42 │   12.82 ms │                                      13.08 ms │     no change │
└───────────┴────────────┴───────────────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                                            │ 26387.39ms │
│ Total Time (claude_single-distinct-to-groupby-allow-count)   │ 26101.22ms │
│ Average Time (HEAD)                                          │   613.66ms │
│ Average Time (claude_single-distinct-to-groupby-allow-count) │   607.01ms │
│ Queries Faster                                               │          3 │
│ Queries Slower                                               │          1 │
│ Queries with No Change                                       │         39 │
│ Queries with Failure                                         │          0 │
└──────────────────────────────────────────────────────────────┴────────────┘

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

Comparing HEAD and claude_single-distinct-to-groupby-allow-count
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃                                   HEAD ┃ claude_single-distinct-to-groupby-allow-count ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0  │           1.38 / 4.40 ±5.86 / 16.12 ms │                  1.22 / 3.94 ±5.36 / 14.66 ms │ +1.12x faster │
│ QQuery 1  │         12.34 / 12.80 ±0.27 / 13.14 ms │                11.86 / 12.01 ±0.12 / 12.20 ms │ +1.07x faster │
│ QQuery 2  │         37.35 / 37.99 ±0.55 / 38.93 ms │                36.73 / 36.90 ±0.12 / 37.05 ms │     no change │
│ QQuery 3  │         33.25 / 34.03 ±0.82 / 35.47 ms │                31.14 / 31.35 ±0.14 / 31.54 ms │ +1.09x faster │
│ QQuery 4  │      265.05 / 269.97 ±4.66 / 276.23 ms │             221.44 / 231.96 ±8.16 / 245.31 ms │ +1.16x faster │
│ QQuery 5  │     276.49 / 288.95 ±13.08 / 309.95 ms │             269.41 / 274.36 ±4.16 / 280.38 ms │ +1.05x faster │
│ QQuery 6  │            1.28 / 1.41 ±0.20 / 1.81 ms │                   1.28 / 1.44 ±0.24 / 1.91 ms │     no change │
│ QQuery 7  │         13.28 / 13.35 ±0.06 / 13.47 ms │                13.09 / 13.20 ±0.07 / 13.29 ms │     no change │
│ QQuery 8  │     337.55 / 351.39 ±10.86 / 361.65 ms │           330.68 / 414.89 ±159.42 / 733.70 ms │  1.18x slower │
│ QQuery 9  │     452.80 / 476.14 ±19.03 / 503.53 ms │             447.81 / 454.59 ±6.52 / 466.88 ms │     no change │
│ QQuery 10 │         69.67 / 75.80 ±7.42 / 87.62 ms │                69.38 / 72.74 ±4.97 / 82.59 ms │     no change │
│ QQuery 11 │         80.87 / 81.53 ±0.43 / 82.17 ms │                80.40 / 81.73 ±2.17 / 86.04 ms │     no change │
│ QQuery 12 │      265.72 / 273.17 ±6.19 / 281.39 ms │             266.16 / 269.76 ±3.69 / 275.76 ms │     no change │
│ QQuery 13 │      978.95 / 984.79 ±4.26 / 992.10 ms │            959.23 / 972.59 ±11.09 / 989.55 ms │     no change │
│ QQuery 14 │      281.40 / 289.20 ±5.44 / 297.13 ms │            287.72 / 311.18 ±15.57 / 333.71 ms │  1.08x slower │
│ QQuery 15 │      266.83 / 271.93 ±2.95 / 275.50 ms │            260.92 / 274.45 ±10.73 / 292.43 ms │     no change │
│ QQuery 16 │  1228.08 / 1270.69 ±27.81 / 1314.41 ms │         1196.31 / 1231.12 ±19.58 / 1251.73 ms │     no change │
│ QQuery 17 │     916.26 / 944.27 ±23.43 / 979.60 ms │            890.67 / 931.33 ±22.36 / 957.69 ms │     no change │
│ QQuery 18 │  2497.04 / 2549.71 ±59.07 / 2659.37 ms │        2464.68 / 2619.87 ±114.28 / 2736.65 ms │     no change │
│ QQuery 19 │         28.04 / 29.82 ±2.46 / 34.68 ms │                30.09 / 30.96 ±0.56 / 31.67 ms │     no change │
│ QQuery 20 │      528.22 / 535.30 ±4.58 / 540.87 ms │            524.86 / 548.63 ±18.95 / 574.17 ms │     no change │
│ QQuery 21 │      516.39 / 520.17 ±3.16 / 525.39 ms │             512.66 / 517.24 ±4.36 / 525.02 ms │     no change │
│ QQuery 22 │      980.18 / 986.55 ±4.83 / 992.72 ms │            974.68 / 988.63 ±9.98 / 1003.70 ms │     no change │
│ QQuery 23 │  3061.15 / 3176.07 ±88.50 / 3302.98 ms │         3010.47 / 3045.11 ±30.25 / 3090.12 ms │     no change │
│ QQuery 24 │         42.00 / 42.34 ±0.44 / 43.20 ms │                41.44 / 43.22 ±3.08 / 49.38 ms │     no change │
│ QQuery 25 │      110.73 / 116.59 ±7.38 / 130.21 ms │             109.66 / 113.03 ±3.80 / 120.17 ms │     no change │
│ QQuery 26 │         42.25 / 43.27 ±0.79 / 44.67 ms │                41.12 / 41.64 ±0.66 / 42.93 ms │     no change │
│ QQuery 27 │      511.60 / 515.42 ±3.73 / 522.37 ms │             509.48 / 514.21 ±4.78 / 521.97 ms │     no change │
│ QQuery 28 │  2913.69 / 2942.96 ±24.29 / 2976.11 ms │         2885.57 / 2943.07 ±47.48 / 3016.49 ms │     no change │
│ QQuery 29 │        41.01 / 52.70 ±10.75 / 69.66 ms │                41.29 / 46.47 ±8.04 / 62.43 ms │ +1.13x faster │
│ QQuery 30 │      298.78 / 305.77 ±4.66 / 313.00 ms │            296.46 / 317.23 ±30.89 / 378.34 ms │     no change │
│ QQuery 31 │      273.81 / 287.28 ±7.37 / 294.32 ms │             284.70 / 291.53 ±4.80 / 296.70 ms │     no change │
│ QQuery 32 │  3254.62 / 3304.71 ±50.88 / 3397.00 ms │        3309.85 / 3514.92 ±160.53 / 3712.98 ms │  1.06x slower │
│ QQuery 33 │ 2515.41 / 2697.29 ±162.83 / 2974.36 ms │         2534.95 / 2607.56 ±78.80 / 2758.34 ms │     no change │
│ QQuery 34 │ 2659.40 / 2764.19 ±119.47 / 2950.04 ms │         2562.43 / 2636.57 ±48.78 / 2684.60 ms │     no change │
│ QQuery 35 │      280.03 / 288.16 ±6.99 / 297.41 ms │            278.34 / 289.88 ±10.71 / 308.26 ms │     no change │
│ QQuery 36 │         65.80 / 72.10 ±3.65 / 75.62 ms │                65.81 / 69.11 ±2.82 / 72.45 ms │     no change │
│ QQuery 37 │         35.17 / 35.91 ±0.46 / 36.61 ms │               35.47 / 44.91 ±17.07 / 79.01 ms │  1.25x slower │
│ QQuery 38 │       39.97 / 54.20 ±24.14 / 102.34 ms │                40.54 / 43.23 ±1.55 / 45.17 ms │ +1.25x faster │
│ QQuery 39 │      133.36 / 140.02 ±4.66 / 147.49 ms │             130.60 / 134.20 ±3.25 / 140.32 ms │     no change │
│ QQuery 40 │         13.78 / 14.17 ±0.25 / 14.52 ms │                13.90 / 14.46 ±0.38 / 15.06 ms │     no change │
│ QQuery 41 │         13.60 / 13.82 ±0.18 / 14.12 ms │                13.63 / 14.37 ±0.88 / 16.03 ms │     no change │
│ QQuery 42 │         12.82 / 18.13 ±9.98 / 38.07 ms │                13.08 / 14.22 ±1.63 / 17.42 ms │ +1.27x faster │
└───────────┴────────────────────────────────────────┴───────────────────────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                                            ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                                            │ 27188.49ms │
│ Total Time (claude_single-distinct-to-groupby-allow-count)   │ 27063.85ms │
│ Average Time (HEAD)                                          │   632.29ms │
│ Average Time (claude_single-distinct-to-groupby-allow-count) │   629.39ms │
│ Queries Faster                                               │          8 │
│ Queries Slower                                               │          4 │
│ Queries with No Change                                       │         31 │
│ Queries with Failure                                         │          0 │
└──────────────────────────────────────────────────────────────┴────────────┘

Memory Pool Peaks

Peak MemoryPool reservation per query — what DataFusion's accounting believes it reserved. Recorded only when the benchmark runs with DATAFUSION_RUNTIME_MEMORY_LIMIT set.

Base: da89c7c (merge-base) | Changed: claude/single-distinct-to-groupby-allow-count

clickbench_partitioned

Query Base Changed Change
Query 0 0 B 0 B 0.0%
Query 1 104 B 104 B +0.0%
Query 2 936 B 936 B +0.0%
Query 3 312 B 312 B +0.0%
Query 4 757.7 MiB 741.3 MiB -2.2%
Query 5 1.2 GiB 1.2 GiB -0.4%
Query 6 0 B 0 B 0.0%
Query 7 40.1 MiB 60.2 MiB +50.1%
Query 8 873.1 MiB 852.4 MiB -2.4%
Query 9 551.7 MiB 551.7 MiB +0.0%
Query 10 107.6 MiB 111.0 MiB +3.2%
Query 11 113.2 MiB 115.9 MiB +2.4%
Query 12 1.3 GiB 1.3 GiB -0.2%
Query 13 998.5 MiB 1.1 GiB +8.5%
Query 14 1.3 GiB 1.3 GiB -1.3%
Query 15 1.1 GiB 1.2 GiB +1.9%
Query 16 1.9 GiB 1.7 GiB -10.3%
Query 17 2.0 GiB 1.9 GiB -1.2%
Query 18 2.0 GiB 2.0 GiB -2.9%
Query 19 0 B 0 B 0.0%
Query 20 104 B 104 B +0.0%
Query 21 3.3 MiB 3.3 MiB -0.0%
Query 22 3.1 MiB 7.3 MiB +132.2%
Query 23 26.8 MiB 25.7 MiB -4.0%
Query 24 58.8 MiB 58.6 MiB -0.4%
Query 25 170.3 MiB 173.2 MiB +1.7%
Query 26 59.9 MiB 60.3 MiB +0.6%
Query 27 2.2 MiB 2.2 MiB +0.0%
Query 28 1.5 GiB 1.4 GiB -3.7%
Query 29 624 B 624 B +0.0%
Query 30 698.5 MiB 707.1 MiB +1.2%
Query 31 1.5 GiB 1.5 GiB +3.2%
Query 32 926.9 MiB 966.9 MiB +4.3%
Query 33 2.0 GiB 2.1 GiB +5.2%
Query 34 2.1 GiB 2.1 GiB -2.6%
Query 35 612.0 MiB 622.3 MiB +1.7%
Query 36 113.6 MiB 111.1 MiB -2.2%
Query 37 6.9 MiB 6.9 MiB +0.0%
Query 38 5.2 MiB 5.2 MiB -0.4%
Query 39 297.5 MiB 297.8 MiB +0.1%
Query 40 2.0 MiB 1.8 MiB -8.3%
Query 41 3.1 MiB 3.1 MiB +0.0%
Query 42 1.6 MiB 1.6 MiB +5.1%

Pool accounting vs. process RSS

Max pool peak is the largest reservation any single query in the run reached; peak RSS covers the whole invocation, including data loading and allocator retention, and the two high-water marks need not coincide in time. The gap is therefore an upper bound on what the pool did not account for, not a measurement of it.

Benchmark Side Max pool peak Peak RSS Gap RSS / pool
clickbench_partitioned base (da89c7c (merge-base)) 2.1 GiB 8.7 GiB 6.6 GiB 4.1×
clickbench_partitioned changed (claude/single-distinct-to-groupby-allow-count) 2.1 GiB 8.6 GiB 6.4 GiB 4.0×
Resource Usage

clickbench_partitioned — base (merge-base)

Metric Value
Wall time 140.0s
Peak memory 8.7 GiB
Avg memory 5.3 GiB
CPU user 1382.5s
CPU sys 131.6s
Peak spill 0 B

clickbench_partitioned — branch

Metric Value
Wall time 140.0s
Peak memory 8.6 GiB
Avg memory 5.1 GiB
CPU user 1376.6s
CPU sys 132.7s
Peak spill 0 B

File an issue against this benchmark runner

adriangb added a commit to pydantic/datafusion that referenced this pull request Sep 1, 2026
The two grouped `COUNT(DISTINCT <string>)` memory limit tests only reach
the per group accumulators while
`single_distinct_aggregation_to_group_by` declines to rewrite the query.
They leant on `count(*)` for that, which the rule rejects only because
`count` is missing from the `sum`/`min`/`max` allow list.
apache#24859 proposes adding `count` to that list, which would
rewrite the query, remove the accumulators, and leave both tests passing
at any memory limit while still looking like they test something.

Aggregate `avg(payload)` over a new `Int64` column instead. `avg` cannot
be added to that list: the rule re-aggregates its own partial results
over the deduplicated inner group by, and averaging per group averages of
different sizes gives the wrong answer. That is why ClickBench Q9 keeps
its distinct aggregate under apache#24859.

Verified from the physical plan with apache#24859 cherry-picked on top of this
branch: the `avg` query still plans as
`aggr=[count(DISTINCT t.value), avg(t.payload)]`, while the `count(*)`
query becomes `aggr=[count(alias1), sum(alias2)]` over an inner
`GROUP BY group_key, value`, and drops from needing ~1.9 MB to ~0.9 MB.

Re-swept both thresholds against the base commit. `Utf8` needs ~35.5 MB
before and ~1.9 MB after; `Utf8View` needs ~123 MB before and ~2.5 MB
after, so the 8 MB and 16 MB limits keep at least 4x margin on each side
and are unchanged.
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) substrait Changes to the substrait crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants