From 450f1897283b450b699e973b74fcbc2bbabce48e Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:14:49 -0500 Subject: [PATCH 1/3] Add a ClickBench extended query for a grouped COUNT(DISTINCT) over a string `COUNT(DISTINCT)` has a specialized `GroupsAccumulator` for the integer types and for no other type. Every other type falls back to `GroupsAccumulatorAdapter`, which holds one boxed `Accumulator`, and therefore one hash table, for each group, so the cost of that fallback scales with the group cardinality. Extended Q2 is the only query in either suite that puts a `COUNT(DISTINCT)` on a non-integer column, and it groups by `BrowserCountry`. Nothing exercises the adapter at a high group cardinality, and nothing covers a lone `COUNT(DISTINCT )` next to a non-distinct `COUNT(*)`: standard Q8, Q10, Q11 and Q13 hold a distinct aggregate that stands alone, Q9 carries an `AVG`, and Q22, the one query that does pair a lone distinct aggregate with a non-distinct count, counts distinct `UserID`, which is an `Int64`. Extended Q14 is that query. It groups by `SearchPhrase` and counts distinct `MobilePhoneModel` beside a `COUNT(*)`. Extended queries are discovered from the directory, so this needs no change to the runner. Co-Authored-By: Claude Opus 5 --- benchmarks/queries/clickbench/README.md | 38 +++++++++++++++++++ .../queries/clickbench/extended/q14.sql | 9 +++++ 2 files changed, 47 insertions(+) create mode 100644 benchmarks/queries/clickbench/extended/q14.sql diff --git a/benchmarks/queries/clickbench/README.md b/benchmarks/queries/clickbench/README.md index 8b3d08b128866..2fd31c0b4c630 100644 --- a/benchmarks/queries/clickbench/README.md +++ b/benchmarks/queries/clickbench/README.md @@ -261,6 +261,44 @@ WHERE "URL" < 'zzzz'; ``` +### Q14: Grouped `COUNT(DISTINCT )` beside a non-distinct `COUNT(*)` + +**Question**: "For the top 10 search phrases by hit count, how many distinct +mobile phone models were used?" + +**Important Query Properties**: A single `COUNT(DISTINCT)` over a high +cardinality string, next to a non-distinct `COUNT(*)`, grouped by a high +cardinality string. + +`COUNT(DISTINCT)` has a specialized `GroupsAccumulator` for the integer types +and for no other type. Every other type falls back to +`GroupsAccumulatorAdapter`, which holds one boxed `Accumulator`, and therefore +one hash table, for each group. The cost of that fallback is per group, so it is +the group cardinality that decides how much it costs. + +Extended Q2 above is the only other query that puts a `COUNT(DISTINCT)` on a +non-integer column, and it groups by `BrowserCountry`. This query groups by +`SearchPhrase` instead, which has far more distinct values, so it exercises the +adapter where its cost actually scales. + +The shape is also the one `SingleDistinctToGroupBy` reasons about. That rule +rewrites a lone distinct aggregate into a two phase group by, which is what +keeps it off the adapter. Standard Q8, Q10, Q11 and Q13 are already rewritten +because their distinct aggregate stands alone, and Q9 is not because it carries +an `AVG`. Q22 is the only standard query that pairs a lone distinct aggregate +with a non-distinct count, and its distinct argument is an `Int64`, which has a +specialized accumulator and never reaches the adapter. So no query in either +suite covers a lone `COUNT(DISTINCT )` next to a non-distinct count. + +```sql +SELECT "SearchPhrase", COUNT(*) AS c, COUNT(DISTINCT "MobilePhoneModel") AS models +FROM hits +WHERE "SearchPhrase" <> '' +GROUP BY "SearchPhrase" +ORDER BY c DESC +LIMIT 10; +``` + ## Data Notes diff --git a/benchmarks/queries/clickbench/extended/q14.sql b/benchmarks/queries/clickbench/extended/q14.sql new file mode 100644 index 0000000000000..4ecc85bd22564 --- /dev/null +++ b/benchmarks/queries/clickbench/extended/q14.sql @@ -0,0 +1,9 @@ +-- Must set for ClickBench hits_partitioned dataset. See https://github.com/apache/datafusion/issues/16591 +-- set datafusion.execution.parquet.binary_as_string = true + +-- A grouped COUNT(DISTINCT ) beside a non-distinct COUNT(*). No query in +-- the standard suite has this shape: q22 is the only one that pairs a lone +-- distinct aggregate with a non-distinct count, and its distinct argument is an +-- Int64, which has a specialized GroupsAccumulator. A string argument has none, +-- so one boxed Accumulator, and one hash table, is built for every group. +SELECT "SearchPhrase", COUNT(*) AS c, COUNT(DISTINCT "MobilePhoneModel") AS models FROM hits WHERE "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; From 23a2181e0d00909f5223f0553dc5ca9fd3c100ef Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:19:04 -0500 Subject: [PATCH 2/3] Backfill clickbench_extended.slt with queries q7-q14 The file only mirrored q0-q6; q7-q13 were added to benchmarks/queries/clickbench/extended/ without a corresponding sqllogictest entry, and this PR's new q14 would have widened that gap. Co-Authored-By: Claude Sonnet 5 --- .../test_files/clickbench_extended.slt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/datafusion/sqllogictest/test_files/clickbench_extended.slt b/datafusion/sqllogictest/test_files/clickbench_extended.slt index c410c98fe8d74..ccc4ef67ed5eb 100644 --- a/datafusion/sqllogictest/test_files/clickbench_extended.slt +++ b/datafusion/sqllogictest/test_files/clickbench_extended.slt @@ -61,6 +61,76 @@ SELECT COUNT(*) AS ShareCount FROM hits WHERE "IsMobile" = 1 AND "MobilePhoneMod ---- 0 +query IIII +SELECT "WatchID", MIN("ResolutionWidth") as wmin, MAX("ResolutionWidth") as wmax, SUM("IsRefresh") as srefresh FROM hits GROUP BY "WatchID" ORDER BY "WatchID" DESC LIMIT 10; +---- +9110818468285196899 0 0 0 +8924809397503602651 0 0 0 +8740403056911509777 0 0 0 +8156744413230856864 0 0 0 +8120543446287442873 0 0 0 +6864353419233967042 0 0 0 +6635790769678439148 0 0 0 +6308646140879811077 0 0 0 +5206346422301499756 0 0 0 +4894690465724379622 0 0 0 + +query III?? +SELECT "RegionID", "UserAgent", "OS", AVG(to_timestamp("ResponseEndTiming")-to_timestamp("ResponseStartTiming")) as avg_response_time, AVG(to_timestamp("ResponseEndTiming")-to_timestamp("ConnectTiming")) as avg_latency FROM hits GROUP BY "RegionID", "UserAgent", "OS" ORDER BY avg_latency DESC limit 10; +---- +229 3 44 0 days 0 hours 0 mins 0.000000000 secs 0 days 0 hours 0 mins 0.000000000 secs +197 7 39 0 days 0 hours 0 mins 0.000000000 secs 0 days 0 hours 0 mins 0.000000000 secs +839 3 2 0 days 0 hours 0 mins 0.000000000 secs 0 days 0 hours 0 mins 0.000000000 secs +839 0 0 0 days 0 hours 0 mins 0.000000000 secs 0 days 0 hours 0 mins 0.000000000 secs +39 7 2 0 days 0 hours 0 mins 0.000000000 secs 0 days 0 hours 0 mins 0.000000000 secs + +query I +SELECT MAX(len) FROM ( + SELECT LENGTH(FIRST_VALUE("URL" ORDER BY "EventTime")) as len + FROM hits + GROUP BY "UserID" +); +---- +72 + +query I +SELECT MAX(len) FROM ( + SELECT LENGTH(FIRST_VALUE("URL" ORDER BY "EventTime")) as len + FROM hits + GROUP BY "OS" +); +---- +57 + +query I +SELECT MAX(fv) FROM ( + SELECT FIRST_VALUE("WatchID" ORDER BY "EventTime") as fv + FROM hits + GROUP BY "UserID" +); +---- +9110818468285196899 + +query I +SELECT MAX(fv) FROM ( + SELECT FIRST_VALUE("WatchID" ORDER BY "EventTime") as fv + FROM hits + GROUP BY "OS" +); +---- +9110818468285196899 + +query I +SELECT SUM("CounterID") AS counter_id_sum +FROM hits +WHERE "URL" < 'zzzz'; +---- +173 + +query TII +SELECT "SearchPhrase", COUNT(*) AS c, COUNT(DISTINCT "MobilePhoneModel") AS models FROM hits WHERE "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; +---- + statement ok drop table hits; From d386092d0651f90b86c3c24dcf18d4d5f76f2894 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:15:29 -0500 Subject: [PATCH 3/3] Mirror extended Q14 into the sql_benchmarks clickbench_extended suite The extended ClickBench queries exist in two runners. `benchmarks/queries/ clickbench/extended/` feeds `dfbench clickbench --queries-path`, and `benchmarks/sql_benchmarks/clickbench_extended/benchmarks/` feeds `benchmark_runner clickbench_extended`. A query added to only one of them is invisible to the other. Add the `.benchmark` file, structurally identical to q13 apart from the query itself. Suite files are discovered from the directory, so nothing else changes: `benchmark_runner clickbench_extended --list` now reports 15 queries. Co-Authored-By: Claude Opus 5 --- .../benchmarks/q14.benchmark | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 benchmarks/sql_benchmarks/clickbench_extended/benchmarks/q14.benchmark diff --git a/benchmarks/sql_benchmarks/clickbench_extended/benchmarks/q14.benchmark b/benchmarks/sql_benchmarks/clickbench_extended/benchmarks/q14.benchmark new file mode 100644 index 0000000000000..203bd1fe7a789 --- /dev/null +++ b/benchmarks/sql_benchmarks/clickbench_extended/benchmarks/q14.benchmark @@ -0,0 +1,22 @@ +name Q14 +group clickbench_extended +subgroup ${CLICKBENCH_TYPE:-single} + +init sql_benchmarks/clickbench/init/set_config.sql + +load sql_benchmarks/clickbench/init/load-${CLICKBENCH_TYPE:-single}.sql + +assert I +SELECT COUNT(*) > 0 from hits; +---- +true + +run +SELECT "SearchPhrase", COUNT(*) AS c, COUNT(DISTINCT "MobilePhoneModel") AS models +FROM hits +WHERE "SearchPhrase" <> '' +GROUP BY "SearchPhrase" +ORDER BY c DESC +LIMIT 10; + +result sql_benchmarks/clickbench_extended/results/q14.csv