Skip to content

[fix](local shuffle) Require hash input for distinct finalize agg without group keys - #66570

Open
924060929 wants to merge 1 commit into
apache:masterfrom
924060929:fix/agg-finalize-distinct-hash
Open

[fix](local shuffle) Require hash input for distinct finalize agg without group keys#66570
924060929 wants to merge 1 commit into
apache:masterfrom
924060929:fix/agg-finalize-distinct-hash

Conversation

@924060929

Copy link
Copy Markdown
Contributor

Problem

With the FE local-shuffle planner enabled (default enable_local_shuffle_planner=true), a scalar COUNT(DISTINCT k) over joins can return a wrong result that grows linearly with parallel_pipeline_task_num (e.g. expected 10, got 30 with 3 tasks).

The bad plan shape:

VAGGREGATE (merge finalize)  output: sum0(multi_distinct_count(k))   -- sums per-instance values
VAGGREGATE (merge finalize)  output: multi_distinct_count(k)         -- no group keys
VHASH JOIN (LEFT OUTER BROADCAST)
  VLOCAL-EXCHANGE (PASSTHROUGH)                                      -- scatters hash-partitioned rows
VHASH JOIN (RIGHT OUTER PARTITIONED)  output: hash-partitioned by k  -- key-aligned here

AggregationNode.enforceAndDeriveLocalExchange gave a NoRequire distribution to a finalize merge agg with no group keys, treating it like COUNT(*). Unlike COUNT(*), a multi_distinct_count finalize agg emits per-instance scalar values that the parent sum0 adds up — correctness requires the input to be hash-partitioned by the distinct key. When a PASSTHROUGH local exchange (broadcast-join probe fan-out) scatters same-key rows across instances, the parent double-counts overlapping keys. The result equals correct × local task count.

The BE-native path was already protected (AggSinkOperatorX::required_data_distribution checks _partition_exprs, and child_breaks_local_key_distribution from a prior fix), so enable_local_shuffle_planner=false was unaffected — only the FE-planned path was wrong.

Root cause

The FE planner used hasKeys (grouping exprs empty?) as the partition-requirement test, but BE's _partition_exprs is non-empty whenever the agg has group keys or DISTINCT aggregates (distribute_expr_lists + has_distinct). The FE fell back to NoRequire for the distinct case, skipping the hash local exchange that the agg needs.

Changes

  • AggregationNode now mirrors BE's _partition_exprs semantics via hasPartitionRequirement() (grouping exprs or multi_distinct_* functions): a finalize agg with a partition requirement demands HASH from its child; only partition-less aggs (COUNT(*)-style) keep NoRequire.
  • The finalize branch that previously trusted the child's distribution now requires HASH explicitly — when the child already provides hash distribution the satisfy() check passes and no LE is inserted, so the common case is unchanged and free.
  • requiresShuffleForCorrectness() now covers DISTINCT aggregates to match BE's is_shuffled_operator().

Tests

  • LocalShuffleNodeCoverageTest: unit coverage for AggregationNode across finalize/LOCAL/FIRST_MERGE phases × distinct/no-distinct × enable_local_exchange_before_agg on/off, plus requiresShuffleForCorrectness cases. Pre-fix the distinct-finalize case asserted NoRequire; post-fix it asserts RequireHash.
  • LocalExchangePlannerTest: sql-level distributed-plan test — the RQG-shaped query (count(distinct) over a shuffle join + a broadcast join with probe forced to PASSTHROUGH) must contain a LOCAL_EXECUTION_HASH_SHUFFLE local exchange below the distinct finalize agg. Verified this test fails without the fix (plan only has PASSTHROUGH) and passes with it.
  • End-to-end on a 3-BE cluster with the RQG dataset: expected 10; the pre-fix behavior (30, scaling with parallel_pipeline_task_num) now returns 10 under all session-var combinations, including parallel_pipeline_task_num=1/2/4/6.

…roup keys

The FE local-shuffle planner handed a NoRequire distribution to a finalize
merge agg that has no group keys but DISTINCT aggregates (e.g.
count(distinct k)). Unlike COUNT(*), such an agg emits per-instance
scalar values that the parent sums (sum0(multi_distinct_count(...))), so
its input must be hash-partitioned by the distinct key. When a
PASSTHROUGH local exchange (e.g. broadcast-join probe fan-out) scatters
same-key rows across instances, the parent double-counts overlapping
keys — result = correct value × local task count.

Mirror BE AggSinkOperatorX::update_operator's _partition_exprs
(grouping exprs, or distinct/distribute exprs): aggs with a partition
requirement must demand HASH from their child; only partition-less aggs
(COUNT(*)-style) keep NoRequire. requiresShuffleForCorrectness now also
covers DISTINCT aggregates to match BE is_shuffled_operator().

Regression tests: AggregationNode unit coverage for every phase/flag
combination, plus a sql-level distributed-plan test asserting the
LOCAL_HASH exchange appears below the distinct finalize agg.
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morrySnow morrySnow changed the title [fix](planner) Require hash input for distinct finalize agg without group keys [fix](local shuffle) Require hash input for distinct finalize agg without group keys Aug 7, 2026
@924060929

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29178 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 87f36c2aa7491acf7cb213c728f8ddd444468218, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17631	3937	3933	3933
q2	1993	391	202	202
q3	10229	1433	791	791
q4	4681	465	337	337
q5	7508	833	546	546
q6	178	167	134	134
q7	736	809	590	590
q8	9320	1589	1467	1467
q9	5315	4065	4118	4065
q10	6765	1628	1364	1364
q11	502	347	345	345
q12	769	573	446	446
q13	18101	3233	2780	2780
q14	266	259	249	249
q15	q16	741	727	656	656
q17	967	1032	1033	1032
q18	6589	5628	5584	5584
q19	1340	1347	1127	1127
q20	791	682	579	579
q21	6322	2851	2642	2642
q22	463	371	309	309
Total cold run time: 101207 ms
Total hot run time: 29178 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4944	4661	4664	4661
q2	297	332	211	211
q3	5032	5310	4639	4639
q4	2139	2231	1420	1420
q5	4687	4445	4399	4399
q6	252	193	139	139
q7	1790	1688	1485	1485
q8	2354	2051	2016	2016
q9	7084	6651	6686	6651
q10	4216	4212	3814	3814
q11	504	367	339	339
q12	686	716	486	486
q13	2942	3249	2794	2794
q14	269	273	245	245
q15	q16	650	669	595	595
q17	1227	1233	1203	1203
q18	12151	11007	11766	11007
q19	1099	1060	1066	1060
q20	2193	2194	1911	1911
q21	5240	4508	4483	4483
q22	557	449	402	402
Total cold run time: 60313 ms
Total hot run time: 53960 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 157704 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 87f36c2aa7491acf7cb213c728f8ddd444468218, data reload: false

query5	4307	588	464	464
query6	455	221	203	203
query7	4911	599	322	322
query8	329	173	159	159
query9	8806	4042	3994	3994
query10	495	373	305	305
query11	5859	2198	1999	1999
query12	162	103	96	96
query13	1274	605	439	439
query14	6081	4293	3977	3977
query14_1	3792	3772	3810	3772
query15	207	222	177	177
query16	995	449	420	420
query17	924	656	513	513
query18	2405	463	329	329
query19	197	186	138	138
query20	103	98	96	96
query21	225	153	131	131
query22	12988	12988	12854	12854
query23	15659	14881	14545	14545
query23_1	14619	14523	14589	14523
query24	7441	1707	1210	1210
query24_1	1244	1241	1218	1218
query25	531	427	342	342
query26	1319	346	212	212
query27	2638	580	371	371
query28	4548	2041	2032	2032
query29	1040	625	499	499
query30	357	265	226	226
query31	1178	1119	1058	1058
query32	109	64	63	63
query33	542	345	287	287
query34	1171	1104	637	637
query35	753	747	634	634
query36	777	779	705	705
query37	161	109	93	93
query38	1842	1786	1684	1684
query39	831	825	793	793
query39_1	809	812	801	801
query40	256	162	144	144
query41	72	70	67	67
query42	97	95	98	95
query43	320	321	277	277
query44	1444	783	765	765
query45	194	172	166	166
query46	1069	1238	740	740
query47	1553	1546	1407	1407
query48	412	394	308	308
query49	607	411	304	304
query50	1018	425	332	332
query51	10454	10561	10309	10309
query52	88	87	83	83
query53	271	275	204	204
query54	314	243	242	242
query55	76	76	70	70
query56	306	321	285	285
query57	1012	980	949	949
query58	279	269	280	269
query59	1560	1667	1422	1422
query60	312	267	249	249
query61	146	146	145	145
query62	402	312	271	271
query63	234	201	191	191
query64	2829	979	821	821
query65	3900	3781	3811	3781
query66	1834	465	369	369
query67	20226	20247	20118	20118
query68	3474	1502	1012	1012
query69	403	302	261	261
query70	858	790	783	783
query71	387	348	298	298
query72	3108	2547	2299	2299
query73	862	763	475	475
query74	4658	4474	4282	4282
query75	2375	2368	1993	1993
query76	2406	1140	748	748
query77	339	364	279	279
query78	11135	11127	10547	10547
query79	1228	1201	753	753
query80	695	585	450	450
query81	457	335	280	280
query82	603	177	136	136
query83	404	333	302	302
query84	330	165	125	125
query85	935	627	537	537
query86	315	241	234	234
query87	1991	1973	1828	1828
query88	3700	2818	2794	2794
query89	379	326	275	275
query90	1933	193	193	193
query91	200	193	159	159
query92	63	61	60	60
query93	1546	1603	947	947
query94	554	340	291	291
query95	802	586	474	474
query96	1061	803	356	356
query97	2453	2421	2344	2344
query98	196	192	184	184
query99	745	725	621	621
Total cold run time: 244011 ms
Total hot run time: 157704 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 23.68 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 87f36c2aa7491acf7cb213c728f8ddd444468218, data reload: false

query1	0.00	0.00	0.01
query2	0.10	0.05	0.04
query3	0.25	0.14	0.14
query4	1.60	0.14	0.13
query5	0.24	0.22	0.22
query6	1.16	0.84	0.84
query7	0.03	0.00	0.01
query8	0.05	0.04	0.03
query9	0.36	0.30	0.31
query10	0.55	0.55	0.54
query11	0.18	0.13	0.13
query12	0.18	0.15	0.14
query13	0.45	0.46	0.47
query14	1.00	1.01	0.98
query15	0.61	0.58	0.58
query16	0.33	0.31	0.31
query17	1.09	1.13	1.10
query18	0.21	0.20	0.19
query19	2.11	1.95	1.92
query20	0.02	0.01	0.02
query21	15.43	0.22	0.13
query22	4.84	0.05	0.05
query23	16.14	0.31	0.13
query24	2.95	0.43	0.32
query25	0.10	0.05	0.04
query26	0.73	0.20	0.14
query27	0.04	0.04	0.04
query28	3.56	0.76	0.35
query29	12.49	3.90	3.14
query30	0.27	0.15	0.15
query31	2.77	0.53	0.32
query32	3.23	0.59	0.49
query33	3.21	3.29	3.14
query34	15.56	3.90	3.23
query35	3.21	3.21	3.18
query36	0.56	0.42	0.42
query37	0.09	0.07	0.06
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.17	0.14	0.14
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.05	0.04	0.03
Total cold run time: 96.13 s
Total hot run time: 23.68 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 72.73% (8/11) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 72.73% (8/11) 🎉
Increment coverage report
Complete coverage report

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants