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

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 292e6e01 perf(aggregate): use hash lookup for scalar field aggregators 
(#349)
292e6e01 is described below

commit 292e6e016fca81a40dd60cb1cae93443fd627fb4
Author: VietCT04 <[email protected]>
AuthorDate: Thu Sep 17 11:18:46 2026 +0800

    perf(aggregate): use hash lookup for scalar field aggregators (#349)
---
 .../compact/aggregate/field_aggregate_utils.cpp    |  71 ++++++++
 .../compact/aggregate/field_aggregate_utils.h      |  32 ++++
 .../compact/aggregate/field_collect_agg.cpp        |  45 ++++-
 .../compact/aggregate/field_collect_agg_test.cpp   | 187 +++++++++++++++++++--
 .../compact/aggregate/field_merge_map_agg.cpp      |  45 ++++-
 .../compact/aggregate/field_merge_map_agg_test.cpp |  81 +++++++++
 6 files changed, 440 insertions(+), 21 deletions(-)

diff --git 
a/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp 
b/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp
index 22e02f10..8dc4b49f 100644
--- a/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp
+++ b/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp
@@ -19,7 +19,10 @@
 
 #include "paimon/core/mergetree/compact/aggregate/field_aggregate_utils.h"
 
+#include <cassert>
+#include <cstdint>
 #include <cstring>
+#include <functional>
 #include <string_view>
 #include <utility>
 #include <vector>
@@ -34,6 +37,7 @@
 #include "paimon/common/utils/checked_cast.h"
 #include "paimon/common/utils/date_time_utils.h"
 #include "paimon/common/utils/fields_comparator.h"
+#include "paimon/common/utils/math.h"
 #include "paimon/memory/bytes.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/status.h"
@@ -123,6 +127,10 @@ Result<bool> EqualMaps(const std::shared_ptr<InternalMap>& 
lhs,
     return true;
 }
 
+size_t CombineHash(size_t lhs, size_t rhs) {
+    return lhs ^ (rhs + static_cast<size_t>(0x9e3779b97f4a7c15ULL) + (lhs << 
6) + (lhs >> 2));
+}
+
 }  // namespace
 
 VariantType FieldAggregateUtils::OwnedBinary(const VariantType& value, 
MemoryPool* pool) {
@@ -244,4 +252,67 @@ Result<bool> FieldAggregateUtils::Equals(const 
VariantType& lhs, const VariantTy
     }
 }
 
+bool FieldAggregateUtils::IsHashableType(const 
std::shared_ptr<arrow::DataType>& type) {
+    switch (type->id()) {
+        case arrow::Type::BOOL:
+        case arrow::Type::INT8:
+        case arrow::Type::INT16:
+        case arrow::Type::INT32:
+        case arrow::Type::DATE32:
+        case arrow::Type::INT64:
+        case arrow::Type::FLOAT:
+        case arrow::Type::DOUBLE:
+        case arrow::Type::STRING:
+        case arrow::Type::BINARY:
+        case arrow::Type::TIMESTAMP:
+            return true;
+        default:
+            return false;
+    }
+}
+
+size_t FieldAggregateUtils::Hash(const VariantType& value,
+                                 const std::shared_ptr<arrow::DataType>& type) 
{
+    assert(IsHashableType(type));
+    size_t result = std::hash<int32_t>{}(static_cast<int32_t>(type->id()));
+    if (DataDefine::IsVariantNull(value)) {
+        return result;
+    }
+    switch (type->id()) {
+        case arrow::Type::BOOL:
+            return CombineHash(result, 
std::hash<bool>{}(DataDefine::GetVariantValue<bool>(value)));
+        case arrow::Type::INT8:
+            return CombineHash(result, 
std::hash<char>{}(DataDefine::GetVariantValue<char>(value)));
+        case arrow::Type::INT16:
+            return CombineHash(result,
+                               
std::hash<int16_t>{}(DataDefine::GetVariantValue<int16_t>(value)));
+        case arrow::Type::INT32:
+        case arrow::Type::DATE32:
+            return CombineHash(result,
+                               
std::hash<int32_t>{}(DataDefine::GetVariantValue<int32_t>(value)));
+        case arrow::Type::INT64:
+            return CombineHash(result,
+                               
std::hash<int64_t>{}(DataDefine::GetVariantValue<int64_t>(value)));
+        case arrow::Type::FLOAT:
+            return CombineHash(result, 
std::hash<int32_t>{}(CanonicalizeFloatToIntBits(
+                                           
DataDefine::GetVariantValue<float>(value))));
+        case arrow::Type::DOUBLE:
+            return CombineHash(result, 
std::hash<int64_t>{}(CanonicalizeDoubleToLongBits(
+                                           
DataDefine::GetVariantValue<double>(value))));
+        case arrow::Type::STRING:
+        case arrow::Type::BINARY:
+            return CombineHash(result,
+                               
std::hash<std::string_view>{}(DataDefine::GetStringView(value)));
+        case arrow::Type::TIMESTAMP: {
+            const auto& timestamp = 
DataDefine::GetVariantValue<Timestamp>(value);
+            return CombineHash(result,
+                               
CombineHash(std::hash<int64_t>{}(timestamp.GetMillisecond()),
+                                           
std::hash<int32_t>{}(timestamp.GetNanoOfMillisecond())));
+        }
+        default:
+            assert(false);
+            return 0;
+    }
+}
+
 }  // namespace paimon
diff --git 
a/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.h 
b/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.h
index e3f74ed7..db03de55 100644
--- a/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.h
+++ b/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.h
@@ -19,6 +19,8 @@
 
 #pragma once
 
+#include <cassert>
+#include <cstddef>
 #include <memory>
 
 #include "arrow/type_fwd.h"
@@ -62,6 +64,36 @@ class FieldAggregateUtils {
     /// @return Whether the values are equal, or an error Status.
     static Result<bool> Equals(const VariantType& lhs, const VariantType& rhs,
                                const std::shared_ptr<arrow::DataType>& type);
+
+    /// Return whether values of this logical type have a hash compatible with 
Equals().
+    static bool IsHashableType(const std::shared_ptr<arrow::DataType>& type);
+
+    /// Hash a value according to the equality semantics of its logical type.
+    ///
+    /// The caller must first check IsHashableType(type).
+    static size_t Hash(const VariantType& value, const 
std::shared_ptr<arrow::DataType>& type);
 };
 
+namespace detail {
+
+struct SemanticHash {
+    std::shared_ptr<arrow::DataType> type;
+
+    size_t operator()(const VariantType& value) const {
+        return FieldAggregateUtils::Hash(value, type);
+    }
+};
+
+struct SemanticEqual {
+    std::shared_ptr<arrow::DataType> type;
+
+    bool operator()(const VariantType& lhs, const VariantType& rhs) const {
+        Result<bool> equal = FieldAggregateUtils::Equals(lhs, rhs, type);
+        assert(equal.ok());
+        return equal.value();
+    }
+};
+
+}  // namespace detail
+
 }  // namespace paimon
diff --git a/src/paimon/core/mergetree/compact/aggregate/field_collect_agg.cpp 
b/src/paimon/core/mergetree/compact/aggregate/field_collect_agg.cpp
index ff938b73..919286b7 100644
--- a/src/paimon/core/mergetree/compact/aggregate/field_collect_agg.cpp
+++ b/src/paimon/core/mergetree/compact/aggregate/field_collect_agg.cpp
@@ -19,6 +19,7 @@
 
 #include "paimon/core/mergetree/compact/aggregate/field_collect_agg.h"
 
+#include <unordered_set>
 #include <vector>
 
 #include "arrow/api.h"
@@ -33,9 +34,12 @@
 namespace paimon {
 namespace {
 
-// TODO(liangjie.liang): Hash VariantType by its type so that this scan, the 
key lookup in
-// FieldMergeMapAgg and the keyed upsert in FieldNestedUpdateAgg stop being 
O(n^2). Java only pays
-// that cost for constructed element types and uses HashSet/HashMap for the 
rest.
+constexpr int32_t kHashThreshold = 192;
+using detail::SemanticEqual;
+using detail::SemanticHash;
+
+// Constructed element types stay on the linear path because their equality 
semantics are not
+// covered by FieldAggregateUtils::Hash.
 Result<bool> Contains(const std::vector<VariantType>& values, const 
VariantType& candidate,
                       const std::shared_ptr<arrow::DataType>& element_type) {
     for (const VariantType& value : values) {
@@ -68,6 +72,23 @@ Status AppendArray(const std::shared_ptr<InternalArray>& 
array,
     return Status::OK();
 }
 
+Status AppendArrayWithHash(const std::shared_ptr<InternalArray>& array,
+                           const std::shared_ptr<arrow::DataType>& 
element_type,
+                           std::unordered_set<VariantType, SemanticHash, 
SemanticEqual>* seen,
+                           std::vector<VariantType>* values) {
+    if (!array) {
+        return Status::OK();
+    }
+    for (int32_t i = 0; i < array->Size(); ++i) {
+        PAIMON_ASSIGN_OR_RAISE(VariantType value,
+                               FieldAggregateUtils::GetValue(*array, i, 
element_type));
+        if (seen->insert(value).second) {
+            values->push_back(std::move(value));
+        }
+    }
+    return Status::OK();
+}
+
 }  // namespace
 
 Result<std::unique_ptr<FieldCollectAgg>> FieldCollectAgg::Create(
@@ -111,12 +132,22 @@ Result<VariantType> FieldCollectAgg::AggImpl(const 
VariantType& accumulator,
     std::shared_ptr<InternalArray> input_array =
         input_null ? nullptr
                    : 
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(input_field);
+    int32_t total_size = (accumulator_array ? accumulator_array->Size() : 0) +
+                         (input_array ? input_array->Size() : 0);
     std::vector<VariantType> values;
-    if (accumulator_array) {
-        values.reserve(accumulator_array->Size() + (input_array ? 
input_array->Size() : 0));
+    values.reserve(total_size);
+    if (distinct_ && total_size >= kHashThreshold &&
+        FieldAggregateUtils::IsHashableType(element_type_)) {
+        SemanticHash hasher{element_type_};
+        SemanticEqual equal{element_type_};
+        std::unordered_set<VariantType, SemanticHash, SemanticEqual> seen(0, 
hasher, equal);
+        seen.reserve(total_size);
+        PAIMON_RETURN_NOT_OK(AppendArrayWithHash(accumulator_array, 
element_type_, &seen, &values));
+        PAIMON_RETURN_NOT_OK(AppendArrayWithHash(input_array, element_type_, 
&seen, &values));
+    } else {
+        PAIMON_RETURN_NOT_OK(AppendArray(accumulator_array, element_type_, 
distinct_, &values));
+        PAIMON_RETURN_NOT_OK(AppendArray(input_array, element_type_, 
distinct_, &values));
     }
-    PAIMON_RETURN_NOT_OK(AppendArray(accumulator_array, element_type_, 
distinct_, &values));
-    PAIMON_RETURN_NOT_OK(AppendArray(input_array, element_type_, distinct_, 
&values));
     std::vector<std::shared_ptr<InternalArray>> holders;
     if (accumulator_array) {
         holders.push_back(accumulator_array);
diff --git 
a/src/paimon/core/mergetree/compact/aggregate/field_collect_agg_test.cpp 
b/src/paimon/core/mergetree/compact/aggregate/field_collect_agg_test.cpp
index 33804567..22389696 100644
--- a/src/paimon/core/mergetree/compact/aggregate/field_collect_agg_test.cpp
+++ b/src/paimon/core/mergetree/compact/aggregate/field_collect_agg_test.cpp
@@ -20,6 +20,8 @@
 #include "paimon/core/mergetree/compact/aggregate/field_collect_agg.h"
 
 #include <algorithm>
+#include <cmath>
+#include <cstring>
 #include <memory>
 #include <string>
 #include <utility>
@@ -32,6 +34,7 @@
 #include "paimon/common/data/generic_row.h"
 #include "paimon/common/data/serializer/binary_serializer_utils.h"
 #include "paimon/common/utils/checked_cast.h"
+#include "paimon/common/utils/fields_comparator.h"
 #include "paimon/core/core_options.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/testing/utils/testharness.h"
@@ -60,6 +63,18 @@ Result<std::unique_ptr<FieldCollectAgg>> MakeCollectAgg(bool 
distinct) {
     return FieldCollectAgg::Create(arrow::list(arrow::int32()), options, "f", 
GetDefaultPool());
 }
 
+Result<std::unique_ptr<FieldCollectAgg>> MakeDistinctAgg(
+    const std::shared_ptr<arrow::DataType>& element_type) {
+    PAIMON_ASSIGN_OR_RAISE(CoreOptions options,
+                           CoreOptions::FromMap({{"fields.f.distinct", 
"true"}}));
+    return FieldCollectAgg::Create(arrow::list(element_type), options, "f", 
GetDefaultPool());
+}
+
+VariantType Array(std::vector<VariantType> values) {
+    return VariantType(
+        
checked_pointer_cast<InternalArray>(std::make_shared<GenericArray>(std::move(values))));
+}
+
 }  // namespace
 
 TEST(FieldCollectAggTest, ConcatenatesWithoutReversing) {
@@ -94,6 +109,166 @@ TEST(FieldCollectAggTest, DistinctAndRetractOneOccurrence) 
{
     ASSERT_EQ((std::vector<int32_t>{1, 2, 3}), Values(retract_result));
 }
 
+TEST(FieldCollectAggTest, HashPathPreservesDuplicatesOverlapAndOrder) {
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldCollectAgg> agg, 
MakeCollectAgg(true));
+    std::vector<VariantType> accumulator;
+    std::vector<VariantType> input;
+    for (int32_t i = 0; i < 100; ++i) {
+        accumulator.emplace_back(i % 40);
+        input.emplace_back(20 + (i % 40));
+    }
+
+    ASSERT_OK_AND_ASSIGN(VariantType result,
+                         agg->Agg(IntArray(std::move(accumulator)), 
IntArray(std::move(input))));
+    std::vector<int32_t> expected;
+    for (int32_t i = 0; i < 60; ++i) {
+        expected.push_back(i);
+    }
+    ASSERT_EQ(expected, Values(result));
+}
+
+TEST(FieldCollectAggTest, HashPathHandlesNullsAndStringContent) {
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldCollectAgg> int_agg, 
MakeCollectAgg(true));
+    std::vector<VariantType> accumulator;
+    std::vector<VariantType> input;
+    for (int32_t i = 0; i < 100; ++i) {
+        accumulator.emplace_back(i == 0 ? VariantType(NullType()) : 
VariantType(int32_t{1}));
+        input.emplace_back(i == 0 ? VariantType(NullType()) : 
VariantType(int32_t{2}));
+    }
+    ASSERT_OK_AND_ASSIGN(VariantType int_result, 
int_agg->Agg(IntArray(std::move(accumulator)),
+                                                              
IntArray(std::move(input))));
+    auto int_array = 
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(int_result);
+    ASSERT_EQ(3, int_array->Size());
+    ASSERT_TRUE(int_array->IsNullAt(0));
+    ASSERT_EQ(1, int_array->GetInt(1));
+    ASSERT_EQ(2, int_array->GetInt(2));
+
+    for (const auto& element_type : {arrow::utf8(), arrow::binary()}) {
+        ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldCollectAgg> string_agg,
+                             MakeDistinctAgg(element_type));
+        std::vector<std::string> left(100, "same-content");
+        std::vector<std::string> right(100, "same-content");
+        std::vector<VariantType> left_values;
+        std::vector<VariantType> right_values;
+        for (const auto& value : left) {
+            left_values.emplace_back(std::string_view(value));
+        }
+        for (const auto& value : right) {
+            right_values.emplace_back(std::string_view(value));
+        }
+        ASSERT_OK_AND_ASSIGN(
+            VariantType string_result,
+            string_agg->Agg(IntArray(std::move(left_values)), 
IntArray(std::move(right_values))));
+        auto string_array =
+            
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(string_result);
+        ASSERT_EQ(1, string_array->Size());
+        ASSERT_EQ("same-content", string_array->GetStringView(0));
+    }
+}
+
+TEST(FieldCollectAggTest, HashPathPreservesFloatingPointAndTimestampSemantics) 
{
+    uint32_t float_bits1 = 0x7fc00001U;
+    uint32_t float_bits2 = 0x7fc00002U;
+    uint64_t double_bits1 = 0x7ff8000000000001ULL;
+    uint64_t double_bits2 = 0x7ff8000000000002ULL;
+    float float_nan1;
+    float float_nan2;
+    double double_nan1;
+    double double_nan2;
+    std::memcpy(&float_nan1, &float_bits1, sizeof(float_nan1));
+    std::memcpy(&float_nan2, &float_bits2, sizeof(float_nan2));
+    std::memcpy(&double_nan1, &double_bits1, sizeof(double_nan1));
+    std::memcpy(&double_nan2, &double_bits2, sizeof(double_nan2));
+
+    auto verify_floating_point = [&](const std::shared_ptr<arrow::DataType>& 
type,
+                                     const VariantType& negative_zero, const 
VariantType& nan1,
+                                     const VariantType& positive_zero, const 
VariantType& nan2) {
+        ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldCollectAgg> agg, 
MakeDistinctAgg(type));
+        std::vector<VariantType> accumulator;
+        std::vector<VariantType> input;
+        for (int32_t i = 0; i < 100; ++i) {
+            accumulator.emplace_back(i % 2 == 0 ? negative_zero : nan1);
+            input.emplace_back(i % 2 == 0 ? positive_zero : nan2);
+        }
+        ASSERT_OK_AND_ASSIGN(VariantType result, 
agg->Agg(IntArray(std::move(accumulator)),
+                                                          
IntArray(std::move(input))));
+        auto array = 
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(result);
+        ASSERT_EQ(3, array->Size());
+        if (type->id() == arrow::Type::FLOAT) {
+            ASSERT_EQ(0, 
FieldsComparator::CompareFloatingPoint(array->GetFloat(0), -0.0F));
+            ASSERT_TRUE(std::isnan(array->GetFloat(1)));
+            ASSERT_EQ(0, 
FieldsComparator::CompareFloatingPoint(array->GetFloat(2), +0.0F));
+        } else {
+            ASSERT_EQ(0, 
FieldsComparator::CompareFloatingPoint(array->GetDouble(0), -0.0));
+            ASSERT_TRUE(std::isnan(array->GetDouble(1)));
+            ASSERT_EQ(0, 
FieldsComparator::CompareFloatingPoint(array->GetDouble(2), +0.0));
+        }
+    };
+    verify_floating_point(arrow::float32(), VariantType(-0.0F), 
VariantType(float_nan1),
+                          VariantType(+0.0F), VariantType(float_nan2));
+    verify_floating_point(arrow::float64(), VariantType(-0.0), 
VariantType(double_nan1),
+                          VariantType(+0.0), VariantType(double_nan2));
+
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldCollectAgg> timestamp_agg,
+                         
MakeDistinctAgg(arrow::timestamp(arrow::TimeUnit::NANO)));
+    Timestamp first = Timestamp::FromEpochMillis(100, 7);
+    Timestamp second = Timestamp::FromEpochMillis(101, 0);
+    std::vector<VariantType> timestamps;
+    std::vector<VariantType> more_timestamps;
+    for (int32_t i = 0; i < 100; ++i) {
+        timestamps.emplace_back(first);
+        more_timestamps.emplace_back(i == 0 ? first : second);
+    }
+    ASSERT_OK_AND_ASSIGN(
+        VariantType timestamp_result,
+        timestamp_agg->Agg(IntArray(std::move(timestamps)), 
IntArray(std::move(more_timestamps))));
+    auto timestamp_array =
+        
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(timestamp_result);
+    ASSERT_EQ(2, timestamp_array->Size());
+    ASSERT_EQ(first, timestamp_array->GetTimestamp(0, 9));
+    ASSERT_EQ(second, timestamp_array->GetTimestamp(1, 9));
+}
+
+TEST(FieldCollectAggTest, FallbackDecimalPathPreservesEquality) {
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldCollectAgg> agg,
+                         MakeDistinctAgg(arrow::decimal128(10, 2)));
+    Decimal one = Decimal::FromUnscaledLong(100, 10, 2);
+    Decimal same_value = Decimal::FromUnscaledLong(1, 10, 0);
+    Decimal two = Decimal::FromUnscaledLong(200, 10, 2);
+    std::vector<VariantType> accumulator;
+    std::vector<VariantType> input;
+    for (int32_t i = 0; i < 100; ++i) {
+        accumulator.emplace_back(one);
+        input.emplace_back(i == 0 ? same_value : two);
+    }
+    ASSERT_OK_AND_ASSIGN(VariantType result,
+                         agg->Agg(IntArray(std::move(accumulator)), 
IntArray(std::move(input))));
+    auto array = 
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(result);
+    ASSERT_EQ(2, array->Size());
+    ASSERT_EQ(one, array->GetDecimal(0, 10, 2));
+    ASSERT_EQ(two, array->GetDecimal(1, 10, 2));
+}
+
+TEST(FieldCollectAggTest, FallbackConstructedPathPreservesEquality) {
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldCollectAgg> agg,
+                         MakeDistinctAgg(arrow::list(arrow::int32())));
+    std::vector<VariantType> accumulator;
+    std::vector<VariantType> input;
+    for (int32_t i = 0; i < 100; ++i) {
+        accumulator.emplace_back(Array({int32_t{1}, int32_t{2}}));
+        input.emplace_back(i == 0 ? Array({int32_t{1}, int32_t{2}})
+                                  : Array({int32_t{2}, int32_t{3}}));
+    }
+    ASSERT_OK_AND_ASSIGN(VariantType result,
+                         agg->Agg(IntArray(std::move(accumulator)), 
IntArray(std::move(input))));
+    auto array = 
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(result);
+    ASSERT_EQ(2, array->Size());
+    ASSERT_EQ(1, array->GetArray(0)->GetInt(0));
+    ASSERT_EQ(2, array->GetArray(0)->GetInt(1));
+    ASSERT_EQ(2, array->GetArray(1)->GetInt(0));
+    ASSERT_EQ(3, array->GetArray(1)->GetInt(1));
+}
+
 TEST(FieldCollectAggTest, RejectsNonArrayType) {
     ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap({}));
     ASSERT_NOK(FieldCollectAgg::Create(arrow::int32(), options, "f", 
GetDefaultPool()));
@@ -103,18 +278,6 @@ TEST(FieldCollectAggTest, RejectsNonArrayType) {
 // collection over composite element types.
 namespace {
 
-Result<std::unique_ptr<FieldCollectAgg>> MakeDistinctAgg(
-    const std::shared_ptr<arrow::DataType>& element_type) {
-    PAIMON_ASSIGN_OR_RAISE(CoreOptions options,
-                           CoreOptions::FromMap({{"fields.f.distinct", 
"true"}}));
-    return FieldCollectAgg::Create(arrow::list(element_type), options, "f", 
GetDefaultPool());
-}
-
-VariantType Array(std::vector<VariantType> values) {
-    return VariantType(
-        
checked_pointer_cast<InternalArray>(std::make_shared<GenericArray>(std::move(values))));
-}
-
 VariantType IntStringRow(int32_t id, std::string_view name) {
     std::shared_ptr<GenericRow> row = std::make_shared<GenericRow>(2);
     row->SetField(0, id);
diff --git 
a/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg.cpp 
b/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg.cpp
index 2cd48bab..107d46a0 100644
--- a/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg.cpp
+++ b/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg.cpp
@@ -19,6 +19,7 @@
 
 #include "paimon/core/mergetree/compact/aggregate/field_merge_map_agg.h"
 
+#include <unordered_map>
 #include <utility>
 #include <vector>
 
@@ -33,6 +34,10 @@
 namespace paimon {
 namespace {
 
+constexpr int32_t kHashThreshold = 192;
+using detail::SemanticEqual;
+using detail::SemanticHash;
+
 struct MapEntry {
     VariantType key;
     VariantType value;
@@ -69,6 +74,29 @@ Status PutMap(const std::shared_ptr<InternalMap>& map,
     return Status::OK();
 }
 
+Status PutMapWithHash(
+    const std::shared_ptr<InternalMap>& map, const 
std::shared_ptr<arrow::DataType>& key_type,
+    const std::shared_ptr<arrow::DataType>& value_type,
+    std::unordered_map<VariantType, int32_t, SemanticHash, SemanticEqual>* 
key_index,
+    std::vector<MapEntry>* entries) {
+    std::shared_ptr<InternalArray> keys = map->KeyArray();
+    std::shared_ptr<InternalArray> values = map->ValueArray();
+    for (int32_t i = 0; i < map->Size(); ++i) {
+        PAIMON_ASSIGN_OR_RAISE(VariantType key, 
FieldAggregateUtils::GetValue(*keys, i, key_type));
+        PAIMON_ASSIGN_OR_RAISE(VariantType value,
+                               FieldAggregateUtils::GetValue(*values, i, 
value_type));
+        auto iter = key_index->find(key);
+        if (iter != key_index->end()) {
+            entries->at(iter->second).value = std::move(value);
+        } else {
+            auto new_index = static_cast<int32_t>(entries->size());
+            entries->push_back(MapEntry{std::move(key), std::move(value)});
+            key_index->emplace(entries->back().key, new_index);
+        }
+    }
+    return Status::OK();
+}
+
 VariantType MakeMap(std::vector<MapEntry> entries,
                     std::vector<std::shared_ptr<InternalArray>> key_holders,
                     std::vector<std::shared_ptr<InternalArray>> value_holders) 
{
@@ -119,8 +147,21 @@ Result<VariantType> FieldMergeMapAgg::AggImpl(const 
VariantType& accumulator,
     auto input_map = 
DataDefine::GetVariantValue<std::shared_ptr<InternalMap>>(input_field);
     std::vector<MapEntry> entries;
     entries.reserve(accumulator_map->Size() + input_map->Size());
-    PAIMON_RETURN_NOT_OK(PutMap(accumulator_map, key_type_, value_type_, 
&entries));
-    PAIMON_RETURN_NOT_OK(PutMap(input_map, key_type_, value_type_, &entries));
+    int32_t total_size = accumulator_map->Size() + input_map->Size();
+    if (total_size >= kHashThreshold && 
FieldAggregateUtils::IsHashableType(key_type_)) {
+        SemanticHash hasher{key_type_};
+        SemanticEqual equal{key_type_};
+        std::unordered_map<VariantType, int32_t, SemanticHash, SemanticEqual> 
key_index(0, hasher,
+                                                                               
         equal);
+        key_index.reserve(total_size);
+        PAIMON_RETURN_NOT_OK(
+            PutMapWithHash(accumulator_map, key_type_, value_type_, 
&key_index, &entries));
+        PAIMON_RETURN_NOT_OK(
+            PutMapWithHash(input_map, key_type_, value_type_, &key_index, 
&entries));
+    } else {
+        PAIMON_RETURN_NOT_OK(PutMap(accumulator_map, key_type_, value_type_, 
&entries));
+        PAIMON_RETURN_NOT_OK(PutMap(input_map, key_type_, value_type_, 
&entries));
+    }
     return MakeMap(std::move(entries), {accumulator_map->KeyArray(), 
input_map->KeyArray()},
                    {accumulator_map->ValueArray(), input_map->ValueArray()});
 }
diff --git 
a/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg_test.cpp 
b/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg_test.cpp
index 1dcbd304..e867623b 100644
--- a/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg_test.cpp
+++ b/src/paimon/core/mergetree/compact/aggregate/field_merge_map_agg_test.cpp
@@ -20,6 +20,7 @@
 #include "paimon/core/mergetree/compact/aggregate/field_merge_map_agg.h"
 
 #include <memory>
+#include <string>
 #include <utility>
 #include <vector>
 
@@ -52,6 +53,15 @@ int32_t FindValue(const VariantType& value, int32_t key) {
     return -1;
 }
 
+std::vector<int32_t> Keys(const VariantType& value) {
+    auto map = 
DataDefine::GetVariantValue<std::shared_ptr<InternalMap>>(value);
+    std::vector<int32_t> keys;
+    for (int32_t i = 0; i < map->Size(); ++i) {
+        keys.push_back(map->KeyArray()->GetInt(i));
+    }
+    return keys;
+}
+
 }  // namespace
 
 TEST(FieldMergeMapAggTest, InputOverwritesAndRetractUsesKeys) {
@@ -80,6 +90,72 @@ TEST(FieldMergeMapAggTest, 
InputOverwritesAndRetractUsesKeys) {
     ASSERT_EQ(-1, FindValue(retracted, 2));
 }
 
+TEST(FieldMergeMapAggTest, HashPathDeduplicatesAccumulatorAndPreservesOrder) {
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldMergeMapAgg> agg,
+                         FieldMergeMapAgg::Create(arrow::map(arrow::int32(), 
arrow::int32()), "f",
+                                                  GetDefaultPool()));
+    std::vector<VariantType> accumulator_keys;
+    std::vector<VariantType> accumulator_values;
+    for (int32_t i = 0; i <= 100; ++i) {
+        accumulator_keys.emplace_back(i);
+        accumulator_values.emplace_back(100 + i);
+    }
+    accumulator_keys.emplace_back(10);
+    accumulator_values.emplace_back(1010);
+
+    std::vector<VariantType> input_keys;
+    std::vector<VariantType> input_values;
+    for (int32_t i = 50; i <= 140; ++i) {
+        input_keys.emplace_back(i);
+        input_values.emplace_back(2000 + i);
+    }
+    input_keys.emplace_back(60);
+    input_values.emplace_back(2060);
+
+    ASSERT_OK_AND_ASSIGN(
+        VariantType result,
+        agg->Agg(IntMap(std::move(accumulator_keys), 
std::move(accumulator_values)),
+                 IntMap(std::move(input_keys), std::move(input_values))));
+    std::vector<int32_t> expected_keys;
+    for (int32_t i = 0; i <= 140; ++i) {
+        expected_keys.push_back(i);
+    }
+    ASSERT_EQ(expected_keys, Keys(result));
+    ASSERT_EQ(1010, FindValue(result, 10));
+    ASSERT_EQ(2050, FindValue(result, 50));
+    ASSERT_EQ(2060, FindValue(result, 60));
+    ASSERT_EQ(2140, FindValue(result, 140));
+}
+
+TEST(FieldMergeMapAggTest, HashPathUsesStringContentAndOverwrites) {
+    std::vector<std::shared_ptr<arrow::DataType>> key_types{arrow::utf8(), 
arrow::binary()};
+    for (const auto& key_type : key_types) {
+        ASSERT_OK_AND_ASSIGN(
+            std::unique_ptr<FieldMergeMapAgg> agg,
+            FieldMergeMapAgg::Create(arrow::map(key_type, arrow::int32()), 
"f", GetDefaultPool()));
+        std::vector<std::string> left(100, 
"same-content-with-a-longer-buffer");
+        std::vector<std::string> right(100, 
"same-content-with-a-longer-buffer");
+        std::vector<VariantType> left_keys;
+        std::vector<VariantType> right_keys;
+        std::vector<VariantType> left_values(100, VariantType(int32_t{10}));
+        std::vector<VariantType> right_values(100, VariantType(int32_t{20}));
+        for (const auto& value : left) {
+            left_keys.emplace_back(std::string_view(value));
+        }
+        for (const auto& value : right) {
+            right_keys.emplace_back(std::string_view(value));
+        }
+
+        ASSERT_OK_AND_ASSIGN(VariantType result,
+                             agg->Agg(IntMap(std::move(left_keys), 
std::move(left_values)),
+                                      IntMap(std::move(right_keys), 
std::move(right_values))));
+        auto result_map = 
DataDefine::GetVariantValue<std::shared_ptr<InternalMap>>(result);
+        ASSERT_EQ(1, result_map->Size());
+        ASSERT_EQ("same-content-with-a-longer-buffer", 
result_map->KeyArray()->GetStringView(0));
+        ASSERT_EQ(20, result_map->ValueArray()->GetInt(0));
+    }
+}
+
 TEST(FieldMergeMapAggTest, NullAndTypeValidation) {
     ASSERT_OK_AND_ASSIGN(std::unique_ptr<FieldMergeMapAgg> agg,
                          FieldMergeMapAgg::Create(arrow::map(arrow::int32(), 
arrow::int32()), "f",
@@ -87,6 +163,11 @@ TEST(FieldMergeMapAggTest, NullAndTypeValidation) {
     VariantType map = IntMap({int32_t{1}}, {int32_t{10}});
     ASSERT_OK_AND_ASSIGN(VariantType result, agg->Agg(VariantType(NullType()), 
map));
     ASSERT_EQ(10, FindValue(result, 1));
+    ASSERT_OK_AND_ASSIGN(VariantType accumulator_result, agg->Agg(map, 
VariantType(NullType())));
+    ASSERT_EQ(10, FindValue(accumulator_result, 1));
+    ASSERT_OK_AND_ASSIGN(VariantType null_result,
+                         agg->Agg(VariantType(NullType()), 
VariantType(NullType())));
+    ASSERT_TRUE(DataDefine::IsVariantNull(null_result));
     ASSERT_NOK(FieldMergeMapAgg::Create(arrow::int32(), "f", 
GetDefaultPool()));
 }
 

Reply via email to