This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 9c384e08d32 branch-4.1: [fix](be) Keep all samples when merging
percentile states #68318 (#68475)
9c384e08d32 is described below
commit 9c384e08d32fdd61de9250f3578fcb8c392a33e5
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Sep 24 14:59:20 2026 +0800
branch-4.1: [fix](be) Keep all samples when merging percentile states
#68318 (#68475)
Cherry-picked from #68318
Co-authored-by: Jerry Hu <[email protected]>
---
be/src/util/percentile_util.h | 36 +++++++--
be/test/util/percentile_util_test.cpp | 84 +++++++++++++++++++
.../aggregate/percentile_bucketed_agg_merge.out | 23 ++++++
.../aggregate/percentile_bucketed_agg_merge.groovy | 93 ++++++++++++++++++++++
4 files changed, 228 insertions(+), 8 deletions(-)
diff --git a/be/src/util/percentile_util.h b/be/src/util/percentile_util.h
index 05003e5cb63..5bf9b518df7 100644
--- a/be/src/util/percentile_util.h
+++ b/be/src/util/percentile_util.h
@@ -47,10 +47,15 @@ class Counts {
public:
Counts() = default;
+ // Moves all samples of `other` into this state. `other` may itself be a
merged state whose
+ // samples live in `_sorted_nums_vec`, and either side may still hold
unsorted raw samples.
void merge(Counts* other) {
- if (other != nullptr && !other->_nums.empty()) {
- _sorted_nums_vec.emplace_back(std::move(other->_nums));
+ _move_nums_to_sorted_vec();
+ other->_move_nums_to_sorted_vec();
+ for (auto& nums : other->_sorted_nums_vec) {
+ _sorted_nums_vec.emplace_back(std::move(nums));
}
+ other->_sorted_nums_vec.clear();
}
void increment(Ty key, uint32_t i) {
@@ -66,16 +71,16 @@ public:
void increment_batch(const PaddedPODArray<Ty>& keys) {
_nums.insert(keys.begin(), keys.end()); }
void serialize(BufferWritable& buf) {
- if (!_nums.empty()) {
+ if (_sorted_nums_vec.empty()) {
pdqsort(_nums.begin(), _nums.end());
- size_t size = _nums.size();
- buf.write_binary(size);
- buf.write(reinterpret_cast<const char*>(_nums.data()), sizeof(Ty)
* size);
} else {
- // convert _sorted_nums_vec to _nums and do seiralize again
+ // merge all sorted runs (including the raw samples) into `_nums`
+ _move_nums_to_sorted_vec();
_convert_sorted_num_vec_to_nums();
- serialize(buf);
}
+ size_t size = _nums.size();
+ buf.write_binary(size);
+ buf.write(reinterpret_cast<const char*>(_nums.data()), sizeof(Ty) *
size);
}
void unserialize(BufferReadable& buf) {
@@ -87,9 +92,13 @@ public:
}
double terminate(double quantile) {
+ if (!_sorted_nums_vec.empty()) {
+ _move_nums_to_sorted_vec();
+ }
if (_sorted_nums_vec.size() <= 1) {
if (_sorted_nums_vec.size() == 1) {
_nums = std::move(_sorted_nums_vec[0]);
+ _sorted_nums_vec.clear();
}
if (_nums.empty()) {
@@ -150,6 +159,17 @@ private:
auto operator<=>(const Node& other) const { return value <=>
other.value; }
};
+ void _move_nums_to_sorted_vec() {
+ if (_nums.empty()) {
+ return;
+ }
+ if (!std::is_sorted(_nums.begin(), _nums.end())) {
+ pdqsort(_nums.begin(), _nums.end());
+ }
+ _sorted_nums_vec.emplace_back(std::move(_nums));
+ DCHECK(_nums.empty());
+ }
+
void _convert_sorted_num_vec_to_nums() {
size_t rows = 0;
for (const auto& i : _sorted_nums_vec) {
diff --git a/be/test/util/percentile_util_test.cpp
b/be/test/util/percentile_util_test.cpp
index 5f6126978e6..a00ae2f75a5 100644
--- a/be/test/util/percentile_util_test.cpp
+++ b/be/test/util/percentile_util_test.cpp
@@ -135,6 +135,90 @@ TEST_F(PercentileUtilTest, CountsSerializeMergedState) {
EXPECT_DOUBLE_EQ(9.0, restored.terminate(1.0));
}
+TEST_F(PercentileUtilTest, CountsMergeMergedStates) {
+ // Two merged states whose samples only live in sorted runs, e.g. partial
states
+ // combined by different pipeline instances before the final merge.
+ Counts<int64_t> run0;
+ run0.increment(0);
+ Counts<int64_t> run1;
+ run1.increment(10);
+ run1.increment(0);
+ Counts<int64_t> left;
+ left.merge(&run0);
+ Counts<int64_t> right;
+ right.merge(&run1);
+
+ Counts<int64_t> merged;
+ merged.merge(&left);
+ merged.merge(&right);
+ // samples are [0, 0, 10], (3 - 1) * 0.625 = 1.25
+ EXPECT_DOUBLE_EQ(2.5, merged.terminate(0.625));
+
+ Counts<int64_t> merged_reverse;
+ Counts<int64_t> run2;
+ run2.increment(0);
+ Counts<int64_t> run3;
+ run3.increment(0);
+ run3.increment(10);
+ Counts<int64_t> left_reverse;
+ left_reverse.merge(&run2);
+ Counts<int64_t> right_reverse;
+ right_reverse.merge(&run3);
+ merged_reverse.merge(&right_reverse);
+ merged_reverse.merge(&left_reverse);
+ EXPECT_DOUBLE_EQ(2.5, merged_reverse.terminate(0.625));
+}
+
+TEST_F(PercentileUtilTest, CountsMergeRawStates) {
+ // Both sides hold unsorted raw samples, e.g. update states of different
instances.
+ Counts<int64_t> dst;
+ dst.increment(9);
+ dst.increment(1);
+ Counts<int64_t> src;
+ src.increment(7);
+ src.increment(3);
+ src.increment(5);
+ dst.merge(&src);
+ // samples are [1, 3, 5, 7, 9]
+ EXPECT_DOUBLE_EQ(1.0, dst.terminate(0.0));
+ EXPECT_DOUBLE_EQ(4.0, dst.terminate(0.375));
+ EXPECT_DOUBLE_EQ(5.0, dst.terminate(0.5));
+ EXPECT_DOUBLE_EQ(8.0, dst.terminate(0.875));
+ EXPECT_DOUBLE_EQ(9.0, dst.terminate(1.0));
+
+ Counts<int64_t> single_run_dst;
+ single_run_dst.increment(4);
+ single_run_dst.increment(2);
+ Counts<int64_t> empty_src;
+ single_run_dst.merge(&empty_src);
+ EXPECT_DOUBLE_EQ(3.0, single_run_dst.terminate(0.5));
+}
+
+TEST_F(PercentileUtilTest, CountsSerializeMixedState) {
+ Counts<int64_t> src;
+ src.increment(8);
+ src.increment(2);
+ Counts<int64_t> merged;
+ merged.merge(&src);
+ // raw samples added after a merge must be kept together with the sorted
runs
+ merged.increment(6);
+ merged.increment(4);
+
+ auto col = ColumnString::create();
+ BufferWritable writer(*col);
+ merged.serialize(writer);
+ writer.commit();
+
+ StringRef data(col->get_chars().data(), col->get_chars().size());
+ BufferReadable reader(data);
+ Counts<int64_t> restored;
+ restored.unserialize(reader);
+ // samples are [2, 4, 6, 8]
+ EXPECT_DOUBLE_EQ(2.0, restored.terminate(0.0));
+ EXPECT_DOUBLE_EQ(5.0, restored.terminate(0.5));
+ EXPECT_DOUBLE_EQ(8.0, restored.terminate(1.0));
+}
+
TEST_F(PercentileUtilTest, CheckQuantileBoundary) {
EXPECT_NO_THROW(check_quantile(0.0));
EXPECT_NO_THROW(check_quantile(0.5));
diff --git
a/regression-test/data/query_p0/aggregate/percentile_bucketed_agg_merge.out
b/regression-test/data/query_p0/aggregate/percentile_bucketed_agg_merge.out
new file mode 100644
index 00000000000..f176744d803
--- /dev/null
+++ b/regression-test/data/query_p0/aggregate/percentile_bucketed_agg_merge.out
@@ -0,0 +1,23 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !union_merge --
+6.25
+
+-- !union_merge_group --
+0 6
+1 6.375
+
+-- !percentile_group --
+0 6 [0, 1, 5, 8]
+1 6.375 [0, 2.25, 4.5, 10]
+
+-- !union_merge_no_bucketed --
+6.25
+
+-- !union_merge_group_no_bucketed --
+0 6
+1 6.375
+
+-- !percentile_group_no_bucketed --
+0 6 [0, 1, 5, 8]
+1 6.375 [0, 2.25, 4.5, 10]
+
diff --git
a/regression-test/suites/query_p0/aggregate/percentile_bucketed_agg_merge.groovy
b/regression-test/suites/query_p0/aggregate/percentile_bucketed_agg_merge.groovy
new file mode 100644
index 00000000000..cfe27b6bca9
--- /dev/null
+++
b/regression-test/suites/query_p0/aggregate/percentile_bucketed_agg_merge.groovy
@@ -0,0 +1,93 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Bucketed hash aggregation merges the per-instance states of the same group
in the
+// source operator. PERCENTILE states merged there must keep every sample of
both sides.
+suite("percentile_bucketed_agg_merge") {
+ sql "set experimental_enable_agg_state=true"
+ sql "set enable_bucketed_hash_agg=true"
+ sql "set be_number_for_test=1"
+ sql "set agg_phase=1"
+ sql "set parallel_pipeline_task_num=4"
+ sql "set bucketed_agg_min_input_rows=0"
+ sql "set bucketed_agg_max_group_keys=0"
+ sql "set bucketed_agg_high_card_threshold=1.0"
+
+ sql "DROP TABLE IF EXISTS percentile_bucketed_agg_merge_t"
+ sql """
+ CREATE TABLE percentile_bucketed_agg_merge_t (
+ id INT NOT NULL,
+ shard INT NOT NULL,
+ v INT NOT NULL
+ )
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 8
+ PROPERTIES ('replication_num' = '1')
+ """
+ sql """
+ INSERT INTO percentile_bucketed_agg_merge_t VALUES
+ (1, 0, 0), (2, 1, 0), (4, 1, 10),
+ (5, 0, 7), (6, 1, 3), (7, 0, 1), (8, 1, 9),
+ (9, 0, 5), (10, 1, 2), (11, 0, 8), (12, 1, 6)
+ """
+
+ explain {
+ sql """
+ SELECT shard, PERCENTILE_UNION(PERCENTILE_STATE(v, CAST(0.625 AS
DOUBLE)))
+ FROM percentile_bucketed_agg_merge_t GROUP BY shard
+ """
+ contains("BUCKETED AGGREGATE")
+ }
+
+ // merged PERCENTILE_UNION states of the same shard come from different
instances
+ order_qt_union_merge """
+ SELECT PERCENTILE_MERGE(s) FROM (
+ SELECT shard, PERCENTILE_UNION(PERCENTILE_STATE(v, CAST(0.625 AS
DOUBLE))) s
+ FROM percentile_bucketed_agg_merge_t GROUP BY shard
+ ) q
+ """
+ order_qt_union_merge_group """
+ SELECT shard, PERCENTILE_MERGE(s) FROM (
+ SELECT shard, PERCENTILE_UNION(PERCENTILE_STATE(v, CAST(0.625 AS
DOUBLE))) s
+ FROM percentile_bucketed_agg_merge_t GROUP BY shard
+ ) q GROUP BY shard
+ """
+ // raw (unmerged) update states of the same shard come from different
instances
+ order_qt_percentile_group """
+ SELECT shard, PERCENTILE(v, 0.625), PERCENTILE_ARRAY(v, [0, 0.25, 0.5,
1])
+ FROM percentile_bucketed_agg_merge_t GROUP BY shard
+ """
+
+ // controls: the same queries without bucketed hash aggregation
+ sql "set enable_bucketed_hash_agg=false"
+ order_qt_union_merge_no_bucketed """
+ SELECT PERCENTILE_MERGE(s) FROM (
+ SELECT shard, PERCENTILE_UNION(PERCENTILE_STATE(v, CAST(0.625 AS
DOUBLE))) s
+ FROM percentile_bucketed_agg_merge_t GROUP BY shard
+ ) q
+ """
+ order_qt_union_merge_group_no_bucketed """
+ SELECT shard, PERCENTILE_MERGE(s) FROM (
+ SELECT shard, PERCENTILE_UNION(PERCENTILE_STATE(v, CAST(0.625 AS
DOUBLE))) s
+ FROM percentile_bucketed_agg_merge_t GROUP BY shard
+ ) q GROUP BY shard
+ """
+ order_qt_percentile_group_no_bucketed """
+ SELECT shard, PERCENTILE(v, 0.625), PERCENTILE_ARRAY(v, [0, 0.25, 0.5,
1])
+ FROM percentile_bucketed_agg_merge_t GROUP BY shard
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]