This is an automated email from the ASF dual-hosted git repository.

mrhhsg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new f3256fdcd2e [improvement](be) Optimize numeric DISTINCT state merging 
(#68350)
f3256fdcd2e is described below

commit f3256fdcd2e67a5ef7ad30b5125f28656e44332e
Author: HappenLee <[email protected]>
AuthorDate: Tue Sep 22 15:26:28 2026 +0800

    [improvement](be) Optimize numeric DISTINCT state merging (#68350)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Problem Summary:
    
    The generic numeric DISTINCT combinator (for example,
    `multi_distinct_sum` over integer columns) copies the entire source hash
    set before merging it. When merging serialized partial aggregates, the
    default implementation first builds a temporary hash set and then copies
    that set again. This adds allocation, hashing, and traversal work in the
    aggregation merge stage.
    
    Insert existing source keys directly without modifying the source.
    Override `deserialize_and_merge` so numeric keys are read directly into
    the destination set. Reserve the known number of incoming keys only when
    the destination is empty: reserving the sum of source and destination
    sizes can unnecessarily grow heavily overlapping sets. Generic/StringRef
    states keep their existing deserialize-then-merge path, including
    copying key bytes into the destination arena. Serialization and NULL
    semantics are unchanged.
    
    An earlier standalone merge-kernel microbenchmark with 131,072 distinct
    int64 keys (Clang 21.1.8, AVX2, nine-run medians) measured serialized
    merging into an empty destination at 9.725 ms before versus 0.944 ms
    with direct deserialization, and full overlap at 6.233 ms versus 0.250
    ms. This uses phmap with a counting standard allocator, not a full Doris
    SQL query; these numbers are not end-to-end latency claims. The
    dedicated `multi_distinct_count` implementation already has its own
    merge optimization and is outside this change.
    
    ### Release note
    
    Reduce temporary hash-set allocations and CPU work when merging generic
    numeric DISTINCT aggregate states.
    
    ### Check List (For Author)
    
    - Test:
    - [x] Unit Test: 16 ASAN tests passed for all five integer widths,
    source preservation, repeated/empty merges, unused numeric scratch sets,
    both Nullable implementations, grouped/selected batch dispatch, and
    generic string ownership.
    - [x] Regression test: one-/two-phase aggregation, grouped/ungrouped
    queries, nullable/non-null inputs, empty/all-null input, and AggState
    merging.
    - Behavior changed:
        - [x] No. SQL results and serialized state formats are unchanged.
    - Does this need documentation?
        - [x] No.
    
    Validation:
    
    - `./run-be-ut.sh -j 96 --run
    
--filter='DistinctNumericMergeTest/*.*:NullImplementations/DistinctMergeDispatchTest.*'`:
    16 tests passed.
    - Header hygiene and clang-format 16 checks passed.
    - `build-support/run-clang-tidy.sh`: no findings on changed code (header
    analyzed with a compile command derived from the new test TU).
    - `./run-regression-test.sh --run -d query_p0/aggregate -s
    test_numeric_distinct_merge -genOut` generated the expected output;
    rerunning without `-genOut` passed all 10 result checks and the
    merge-plan assertion.
    - `./build.sh --be --fe -j 96` completed with ASAN BE and FE Checkstyle
    enabled.
---
 .../exprs/aggregate/aggregate_function_distinct.h  |  28 ++-
 .../aggregate/aggregate_function_distinct_test.cpp | 247 +++++++++++++++++++++
 .../aggregate/test_numeric_distinct_merge.out      |  37 +++
 .../aggregate/test_numeric_distinct_merge.groovy   |  70 ++++++
 4 files changed, 381 insertions(+), 1 deletion(-)

diff --git a/be/src/exprs/aggregate/aggregate_function_distinct.h 
b/be/src/exprs/aggregate/aggregate_function_distinct.h
index c1dfb6bae5d..c2c84934c30 100644
--- a/be/src/exprs/aggregate/aggregate_function_distinct.h
+++ b/be/src/exprs/aggregate/aggregate_function_distinct.h
@@ -72,7 +72,13 @@ struct AggregateFunctionDistinctSingleNumericData {
     void merge(const Self& rhs, Arena&) {
         DCHECK(!stable);
         if constexpr (!stable) {
-            data.merge(Container(rhs.data));
+            // Only an empty destination has a known final size; other sets 
may overlap.
+            if (data.empty() && !rhs.data.empty()) {
+                data.reserve(rhs.data.size());
+            }
+            for (const auto& elem : rhs.data) {
+                data.insert(elem);
+            }
         }
     }
 
@@ -91,6 +97,10 @@ struct AggregateFunctionDistinctSingleNumericData {
         if constexpr (!stable) {
             uint64_t new_size = 0;
             buf.read_var_uint(new_size);
+            // Avoid reserving an upper bound when merging into a nonempty set.
+            if (data.empty() && new_size != 0) {
+                data.reserve(new_size);
+            }
             typename PrimitiveTypeTraits<T>::CppType x;
             for (size_t i = 0; i < new_size; ++i) {
                 buf.read_binary(x);
@@ -99,6 +109,11 @@ struct AggregateFunctionDistinctSingleNumericData {
         }
     }
 
+    void deserialize_and_merge(Self& /*rhs*/, BufferReadable& buf, Arena& 
arena) {
+        // Numeric keys can be inserted directly without building a temporary 
hash set.
+        deserialize(buf, arena);
+    }
+
     MutableColumns get_arguments(const DataTypes& argument_types) const {
         MutableColumns argument_columns;
         argument_columns.emplace_back(argument_types[0]->create_column());
@@ -153,6 +168,12 @@ struct AggregateFunctionDistinctGenericData {
         }
     }
 
+    void deserialize_and_merge(Self& rhs, BufferReadable& buf, Arena& arena) {
+        // deserialize() borrows StringRefs from buf; merge() copies them into 
the arena.
+        rhs.deserialize(buf, arena);
+        merge(rhs, arena);
+    }
+
     void deserialize(BufferReadable& buf, Arena& arena) {
         DCHECK(!stable);
         if constexpr (!stable) {
@@ -308,6 +329,11 @@ public:
         this->data(place).deserialize(buf, arena);
     }
 
+    void deserialize_and_merge(AggregateDataPtr __restrict place, 
AggregateDataPtr __restrict rhs,
+                               BufferReadable& buf, Arena& arena) const 
override {
+        this->data(place).deserialize_and_merge(this->data(rhs), buf, arena);
+    }
+
     void insert_result_into(ConstAggregateDataPtr targetplace, IColumn& to) 
const override {
         // place is essentially an AggregateDataPtr, passed as a 
ConstAggregateDataPtr.
         auto* place = const_cast<AggregateDataPtr>(targetplace);
diff --git a/be/test/exprs/aggregate/aggregate_function_distinct_test.cpp 
b/be/test/exprs/aggregate/aggregate_function_distinct_test.cpp
new file mode 100644
index 00000000000..132c332990b
--- /dev/null
+++ b/be/test/exprs/aggregate/aggregate_function_distinct_test.cpp
@@ -0,0 +1,247 @@
+// 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.
+
+#include "exprs/aggregate/aggregate_function_distinct.h"
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <array>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include "agent/be_exec_version_manager.h"
+#include "core/arena.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "core/string_buffer.hpp"
+#include "exec/common/hash_table/phmap_fwd_decl.h"
+#include "exprs/aggregate/aggregate_function_simple_factory.h"
+#include "testutil/column_helper.h"
+
+namespace doris {
+namespace {
+
+template <typename T>
+class DistinctNumericMergeTest : public testing::Test {};
+
+using IntegerTypes = testing::Types<std::integral_constant<PrimitiveType, 
TYPE_TINYINT>,
+                                    std::integral_constant<PrimitiveType, 
TYPE_SMALLINT>,
+                                    std::integral_constant<PrimitiveType, 
TYPE_INT>,
+                                    std::integral_constant<PrimitiveType, 
TYPE_BIGINT>,
+                                    std::integral_constant<PrimitiveType, 
TYPE_LARGEINT>>;
+TYPED_TEST_SUITE(DistinctNumericMergeTest, IntegerTypes);
+
+TYPED_TEST(DistinctNumericMergeTest, 
PreserveSourceAndDeduplicateRepeatedMerges) {
+    using Data = AggregateFunctionDistinctSingleNumericData<TypeParam::value, 
false>;
+    Arena arena;
+    Data destination;
+    Data source;
+    source.data.insert({1, 2, 3});
+
+    destination.merge(source, arena);
+    EXPECT_EQ(destination.data, source.data);
+    destination.data.insert(4);
+    const auto capacity = destination.data.capacity();
+    destination.merge(source, arena);
+    EXPECT_EQ(destination.data.size(), 4);
+    EXPECT_EQ(destination.data.capacity(), capacity);
+    EXPECT_EQ(source.data.size(), 3);
+    EXPECT_FALSE(source.data.contains(4));
+
+    Data empty;
+    destination.merge(empty, arena);
+    EXPECT_EQ(destination.data.size(), 4);
+    EXPECT_EQ(destination.data.capacity(), capacity);
+    destination.clear();
+    destination.merge(empty, arena);
+    EXPECT_TRUE(destination.data.empty());
+}
+
+TYPED_TEST(DistinctNumericMergeTest, 
DeserializeIntoExistingDestinationWithoutPopulatingScratch) {
+    using Data = AggregateFunctionDistinctSingleNumericData<TypeParam::value, 
false>;
+    Arena arena;
+    Data source;
+    source.data.insert({1, 2, 3});
+    ColumnString serialized;
+    VectorBufferWriter writer(serialized);
+    source.serialize(writer);
+    writer.commit();
+
+    Data destination;
+    Data scratch;
+    destination.data.insert(4);
+    for (int repeat = 0; repeat < 2; ++repeat) {
+        VectorBufferReader reader(serialized.get_data_at(0));
+        destination.deserialize_and_merge(scratch, reader, arena);
+        EXPECT_EQ(destination.data.size(), 4);
+        for (int value : {1, 2, 3, 4}) {
+            EXPECT_TRUE(destination.data.contains(value));
+        }
+        // The optimization must not materialize the serialized set in the 
scratch state.
+        EXPECT_TRUE(scratch.data.empty());
+        EXPECT_EQ(scratch.data.capacity(), 0);
+    }
+
+    Data empty;
+    ColumnString serialized_empty;
+    VectorBufferWriter empty_writer(serialized_empty);
+    empty.serialize(empty_writer);
+    empty_writer.commit();
+    VectorBufferReader empty_reader(serialized_empty.get_data_at(0));
+    destination.deserialize_and_merge(scratch, empty_reader, arena);
+    EXPECT_EQ(destination.data.size(), 4);
+
+    destination.clear();
+    VectorBufferReader reader(serialized.get_data_at(0));
+    destination.deserialize_and_merge(scratch, reader, arena);
+    EXPECT_EQ(destination.data, source.data);
+    EXPECT_TRUE(scratch.data.empty());
+}
+
+void add_column(const IAggregateFunction& function, AggregateDataPtr place, 
const IColumn& column,
+                Arena& arena) {
+    const IColumn* columns[] = {&column};
+    function.add_batch_single_place(column.size(), place, columns, arena);
+}
+
+class DistinctMergeDispatchTest : public testing::TestWithParam<bool> {
+protected:
+    AggregateFunctionPtr function(const std::string& name, const DataTypePtr& 
input_type,
+                                  const DataTypePtr& result_type, bool 
result_nullable) {
+        AggregateFunctionAttr attr;
+        attr.enable_aggregate_function_null_v2 = GetParam();
+        return AggregateFunctionSimpleFactory::instance().get(
+                name, {input_type}, result_type, result_nullable,
+                BeExecVersionManager::get_newest_version(), attr);
+    }
+};
+
+TEST_P(DistinctMergeDispatchTest, 
NullableSumMergesColumnRangesAndAllNullStates) {
+    auto type = make_nullable(std::make_shared<DataTypeInt64>());
+    auto aggregate = function("multi_distinct_sum", type, type, true);
+    ASSERT_NE(aggregate, nullptr);
+    Arena arena;
+    AggregateFunctionGuard source(aggregate.get());
+    AggregateFunctionGuard destination(aggregate.get());
+    auto serialized = aggregate->get_serialized_type()->create_column();
+
+    auto first = ColumnHelper::create_nullable_column<DataTypeInt64>({1, 2, 2, 
99}, {0, 0, 0, 1});
+    add_column(*aggregate, source.data(), *first, arena);
+    aggregate->serialize_without_key_to_column(source.data(), *serialized);
+    aggregate->reset(source.data());
+    auto second = ColumnHelper::create_nullable_column<DataTypeInt64>({2, 3, 
99}, {0, 0, 1});
+    add_column(*aggregate, source.data(), *second, arena);
+    aggregate->serialize_without_key_to_column(source.data(), *serialized);
+    aggregate->reset(source.data());
+    auto nulls = ColumnHelper::create_nullable_column<DataTypeInt64>({99, 99}, 
{1, 1});
+    add_column(*aggregate, source.data(), *nulls, arena);
+    aggregate->serialize_without_key_to_column(source.data(), *serialized);
+
+    auto initial = ColumnHelper::create_nullable_column<DataTypeInt64>({10}, 
{0});
+    add_column(*aggregate, destination.data(), *initial, arena);
+    for (int repeat = 0; repeat < 2; ++repeat) {
+        aggregate->deserialize_and_merge_from_column(destination.data(), 
*serialized, arena);
+    }
+    auto result = type->create_column();
+    aggregate->insert_result_into(destination.data(), *result);
+    EXPECT_TRUE(ColumnHelper::column_equal(
+            std::move(result), 
ColumnHelper::create_nullable_column<DataTypeInt64>({16}, {0})));
+
+    aggregate->reset(destination.data());
+    aggregate->deserialize_and_merge_from_column_range(destination.data(), 
*serialized, 2, 2,
+                                                       arena);
+    result = type->create_column();
+    aggregate->insert_result_into(destination.data(), *result);
+    EXPECT_TRUE(ColumnHelper::column_equal(
+            std::move(result), 
ColumnHelper::create_nullable_column<DataTypeInt64>({0}, {1})));
+}
+
+TEST_P(DistinctMergeDispatchTest, GroupedAndSelectedBatchMerges) {
+    for (bool nullable : {false, true}) {
+        DataTypePtr type = std::make_shared<DataTypeInt64>();
+        if (nullable) {
+            type = make_nullable(type);
+        }
+        auto aggregate = function("multi_distinct_sum", type, type, nullable);
+        ASSERT_NE(aggregate, nullptr);
+        Arena arena;
+        AggregateFunctionGuard source(aggregate.get());
+        AggregateFunctionGuard destination(aggregate.get());
+        auto serialized = aggregate->get_serialized_type()->create_column();
+        for (const auto& values : {std::vector<Int64> {1, 2, 2}, 
std::vector<Int64> {2, 3}}) {
+            auto column = type->create_column();
+            for (auto value : values) {
+                column->insert(Field::create_field<TYPE_BIGINT>(value));
+            }
+            aggregate->reset(source.data());
+            add_column(*aggregate, source.data(), *column, arena);
+            aggregate->serialize_without_key_to_column(source.data(), 
*serialized);
+        }
+
+        auto* scratch = reinterpret_cast<AggregateDataPtr>(
+                arena.aligned_alloc(2 * aggregate->size_of_data(), 
aggregate->align_of_data()));
+        std::array<AggregateDataPtr, 2> places {destination.data(), 
destination.data()};
+        aggregate->deserialize_and_merge_vec(places.data(), 0, scratch, 
serialized.get(), arena, 2);
+        auto result = type->create_column();
+        aggregate->insert_result_into(destination.data(), *result);
+        EXPECT_EQ(type->to_string(*result, 0), "6");
+
+        aggregate->reset(destination.data());
+        places[0] = nullptr;
+        aggregate->deserialize_and_merge_vec_selected(places.data(), 0, 
scratch, serialized.get(),
+                                                      arena, 2);
+        result = type->create_column();
+        aggregate->insert_result_into(destination.data(), *result);
+        EXPECT_EQ(type->to_string(*result, 0), "5");
+    }
+}
+
+TEST_P(DistinctMergeDispatchTest, 
GenericStringStateOwnsKeysAfterInputBufferIsReleased) {
+    auto type = std::make_shared<DataTypeString>();
+    auto aggregate = function("multi_distinct_min", type, type, false);
+    ASSERT_NE(aggregate, nullptr);
+    auto serialized = ColumnString::create();
+    const std::string first(512, 'a');
+    const std::string second(512, 'b');
+    {
+        Arena source_arena;
+        AggregateFunctionGuard source(aggregate.get());
+        auto column = ColumnHelper::create_column<DataTypeString>({first, 
second, first});
+        add_column(*aggregate, source.data(), *column, source_arena);
+        aggregate->serialize_without_key_to_column(source.data(), *serialized);
+    }
+
+    Arena arena;
+    AggregateFunctionGuard destination(aggregate.get());
+    aggregate->deserialize_and_merge_from_column(destination.data(), 
*serialized, arena);
+    std::fill(serialized->get_chars().begin(), serialized->get_chars().end(), 
'?');
+    serialized.reset();
+    auto result = type->create_column();
+    aggregate->insert_result_into(destination.data(), *result);
+    EXPECT_EQ(result->get_data_at(0).to_string(), first);
+}
+
+INSTANTIATE_TEST_SUITE_P(NullImplementations, DistinctMergeDispatchTest, 
testing::Bool());
+
+} // namespace
+} // namespace doris
diff --git 
a/regression-test/data/query_p0/aggregate/test_numeric_distinct_merge.out 
b/regression-test/data/query_p0/aggregate/test_numeric_distinct_merge.out
new file mode 100644
index 00000000000..bbfe6f8e6c7
--- /dev/null
+++ b/regression-test/data/query_p0/aggregate/test_numeric_distinct_merge.out
@@ -0,0 +1,37 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !integer_types_1 --
+9      9       9       9       9       9
+
+-- !grouped_1 --
+\N     4       4
+0      3       3
+1      4       4
+2      \N      0
+
+-- !all_null_1 --
+\N
+
+-- !empty_1 --
+\N     \N
+
+-- !state_merge_1 --
+9
+
+-- !integer_types_2 --
+9      9       9       9       9       9
+
+-- !grouped_2 --
+\N     4       4
+0      3       3
+1      4       4
+2      \N      0
+
+-- !all_null_2 --
+\N
+
+-- !empty_2 --
+\N     \N
+
+-- !state_merge_2 --
+9
+
diff --git 
a/regression-test/suites/query_p0/aggregate/test_numeric_distinct_merge.groovy 
b/regression-test/suites/query_p0/aggregate/test_numeric_distinct_merge.groovy
new file mode 100644
index 00000000000..5c6b765dfe3
--- /dev/null
+++ 
b/regression-test/suites/query_p0/aggregate/test_numeric_distinct_merge.groovy
@@ -0,0 +1,70 @@
+// 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.
+
+suite("test_numeric_distinct_merge") {
+    sql "DROP TABLE IF EXISTS test_numeric_distinct_merge"
+    sql """
+        CREATE TABLE test_numeric_distinct_merge (
+            id INT NOT NULL,
+            g INT NULL,
+            v BIGINT NULL,
+            nonnull_v BIGINT NOT NULL
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 4
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        INSERT INTO test_numeric_distinct_merge VALUES
+            (1, 0, 1, 1), (2, 0, 2, 2), (3, 0, 2, 2), (4, 0, NULL, 0),
+            (5, 1, 2, 2), (6, 1, 3, 3), (7, 1, -1, -1), (8, 1, NULL, 0),
+            (9, 2, NULL, 0), (10, NULL, 4, 4), (11, NULL, 4, 4), (12, NULL, 
NULL, 0)
+    """
+
+    for (phase in [1, 2]) {
+        sql "SET agg_phase = ${phase}"
+
+        "order_qt_integer_types_${phase}" """
+            SELECT multi_distinct_sum(CAST(v AS TINYINT)),
+                   multi_distinct_sum(CAST(v AS SMALLINT)),
+                   multi_distinct_sum(CAST(v AS INT)),
+                   multi_distinct_sum(v),
+                   multi_distinct_sum(CAST(v AS LARGEINT)),
+                   multi_distinct_sum(nonnull_v)
+            FROM test_numeric_distinct_merge
+        """
+        "order_qt_grouped_${phase}" """
+            SELECT g, multi_distinct_sum(v), multi_distinct_sum(nonnull_v)
+            FROM test_numeric_distinct_merge GROUP BY g
+        """
+        "order_qt_all_null_${phase}" """
+            SELECT multi_distinct_sum(v) FROM test_numeric_distinct_merge 
WHERE g = 2
+        """
+        "order_qt_empty_${phase}" """
+            SELECT multi_distinct_sum(v), multi_distinct_sum(nonnull_v)
+            FROM test_numeric_distinct_merge WHERE id < 0
+        """
+        "order_qt_state_merge_${phase}" """
+            SELECT multi_distinct_sum_merge(multi_distinct_sum_state(v))
+            FROM test_numeric_distinct_merge
+        """
+    }
+
+    explain {
+        sql "SELECT multi_distinct_sum(v) FROM test_numeric_distinct_merge"
+        contains "merge finalize"
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to