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

HappenLee 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 97705a7f457 [improvement](be) Adapt bucket counts for bucketed 
aggregation (#69425)
97705a7f457 is described below

commit 97705a7f4578dd15ff277b6a5e8dd70199edb996
Author: HappenLee <[email protected]>
AuthorDate: Mon Oct 26 19:07:01 2026 +0800

    [improvement](be) Adapt bucket counts for bucketed aggregation (#69425)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: N/A
    
    Problem Summary: Bucketed aggregation allocates 256 hash tables per sink 
even for serial execution, and routing with mixed-hash bits 24-31 overlaps 
phmap probe-index bits as tables grow. Add bucketed_agg_num_buckets with 
automatic or explicit power-of-two bucket selection, capture it in the plan and 
forward it in an optional Thrift field. Automatic mode uses one bucket when 
both sink and source have one instance on a BE, and 256 otherwise; absent 
fields retain 256 buckets. Route with  [...]
    
    ### Release note
    
    Add the bucketed_agg_num_buckets session variable. The default 0 selects 
one bucket for a single sink and source on a BE, and 256 otherwise. Explicit 
values must be powers of two from 1 through 256. SQL aggregation results are 
unchanged.
    
    ### Check List (For Author)
    
    - Test: TeamCity COMPILE, FE UT, BE UT, Cloud UT, P0, Cloud P0, Vault P0, 
External and NonConcurrent regression, performance, and BE/FE coverage checks 
passed on 576b4676128a51a6f773adb903f3af730e1973fd. GitHub formatting, 
Checkstyle, license and code-review checks passed. Independent GCC 15.1.0 -O3 
-Werror compile reproducer passed after the completion-flag allocation fix.
    - Behavior changed: Yes; automatic bucket selection and a per-session 
bucket-count override, with unchanged aggregation semantics.
    - Does this need documentation: Yes; session-variable reference. The inline 
setting description is included; the website documentation PR remains pending.
---
 .../bucketed_aggregation_sink_operator.cpp         |  143 ++-
 .../operator/bucketed_aggregation_sink_operator.h  |   10 +-
 .../bucketed_aggregation_source_operator.cpp       |   19 +-
 be/src/exec/pipeline/dependency.cpp                |   35 +-
 be/src/exec/pipeline/dependency.h                  |   23 +-
 be/test/exec/operator/agg_operator_test.cpp        |    4 +-
 .../pipeline/bucketed_agg_shared_state_test.cpp    |   91 ++
 .../glue/translator/PhysicalPlanTranslator.java    |    3 +-
 .../doris/planner/BucketedAggregationNode.java     |    7 +-
 .../java/org/apache/doris/qe/SessionVariable.java  |   23 +-
 .../BucketedAggregateTranslatorTest.java           |   48 +
 .../rules/analysis/AnalyzeWhereSubqueryTest.java   |    5 +-
 .../doris/qe/SessionVariableBucketedAggTest.java   |   65 +
 gensrc/thrift/PlanNodes.thrift                     |    3 +
 .../agg_strategy/bucketed_hash_agg_num_buckets.out | 1236 ++++++++++++++++++++
 .../bucketed_hash_agg_num_buckets.groovy           |  177 +++
 16 files changed, 1801 insertions(+), 91 deletions(-)

diff --git a/be/src/exec/operator/bucketed_aggregation_sink_operator.cpp 
b/be/src/exec/operator/bucketed_aggregation_sink_operator.cpp
index 9c76ec7e8a7..2471735a80f 100644
--- a/be/src/exec/operator/bucketed_aggregation_sink_operator.cpp
+++ b/be/src/exec/operator/bucketed_aggregation_sink_operator.cpp
@@ -54,6 +54,7 @@ Status BucketedAggSinkLocalState::init(RuntimeState* state, 
LocalSinkStateInfo&
     _expr_timer = ADD_TIMER(Base::custom_profile(), "ExprTime");
     _hash_table_compute_timer = ADD_TIMER(Base::custom_profile(), 
"HashTableComputeTime");
     _hash_table_emplace_timer = ADD_TIMER(Base::custom_profile(), 
"HashTableEmplaceTime");
+    _partition_timer = ADD_TIMER(Base::custom_profile(), "PartitionTime");
     _hash_table_input_counter =
             ADD_COUNTER(Base::custom_profile(), "HashTableInputCount", 
TUnit::UNIT);
     _memory_usage_arena = ADD_COUNTER(custom_profile(), "MemoryUsageArena", 
TUnit::BYTES);
@@ -72,29 +73,40 @@ Status BucketedAggSinkLocalState::open(RuntimeState* state) 
{
     // Initialize per-instance data and shared metadata. Multiple sink 
instances call open()
     // concurrently, so all shared-state writes must be inside call_once to 
avoid data races.
     // init_instances stores the init status in shared state so all threads 
see failures.
-    RETURN_IF_ERROR(shared_state.init_instances(state->task_num(), [&]() -> 
Status {
-        // Copy metadata to shared state (once, from the first instance to 
reach here).
-        shared_state.align_aggregate_states = p._align_aggregate_states;
-        shared_state.total_size_of_aggregate_states = 
p._total_size_of_aggregate_states;
-        shared_state.offsets_of_aggregate_states = 
p._offsets_of_aggregate_states;
-        shared_state.make_nullable_keys = p._make_nullable_keys;
-
-        shared_state.probe_expr_ctxs.resize(p._probe_expr_ctxs.size());
-        for (size_t i = 0; i < shared_state.probe_expr_ctxs.size(); i++) {
-            RETURN_IF_ERROR(p._probe_expr_ctxs[i]->clone(state, 
shared_state.probe_expr_ctxs[i]));
-        }
-
-        for (auto& evaluator : p._aggregate_evaluators) {
-            
shared_state.aggregate_evaluators.push_back(evaluator->clone(state, p._pool));
-        }
-
-        // Detect simple_count: exactly one COUNT(*) / COUNT(slot), with GROUP 
BY present.
-        // Bucketed agg always has GROUP BY (without-key not supported).
-        if (p._aggregate_evaluators.size() == 1 && 
p._aggregate_evaluators[0]->is_simple_count()) {
-            shared_state.use_simple_count = true;
-        }
-        return Status::OK();
-    }));
+    RETURN_IF_ERROR(shared_state.init_instances(
+            state->task_num(),
+            [&]() -> Status {
+                // Copy metadata to shared state (once, from the first 
instance to reach here).
+                shared_state.align_aggregate_states = 
p._align_aggregate_states;
+                shared_state.total_size_of_aggregate_states = 
p._total_size_of_aggregate_states;
+                shared_state.offsets_of_aggregate_states = 
p._offsets_of_aggregate_states;
+                shared_state.make_nullable_keys = p._make_nullable_keys;
+
+                shared_state.probe_expr_ctxs.resize(p._probe_expr_ctxs.size());
+                for (size_t i = 0; i < shared_state.probe_expr_ctxs.size(); 
i++) {
+                    RETURN_IF_ERROR(
+                            p._probe_expr_ctxs[i]->clone(state, 
shared_state.probe_expr_ctxs[i]));
+                }
+
+                for (auto& evaluator : p._aggregate_evaluators) {
+                    
shared_state.aggregate_evaluators.push_back(evaluator->clone(state, p._pool));
+                }
+
+                // Detect simple_count: exactly one COUNT(*) / COUNT(slot), 
with GROUP BY present.
+                // Bucketed agg always has GROUP BY (without-key not 
supported).
+                if (p._aggregate_evaluators.size() == 1 &&
+                    p._aggregate_evaluators[0]->is_simple_count()) {
+                    shared_state.use_simple_count = true;
+                }
+                return Status::OK();
+            },
+            p._num_buckets));
+
+    _num_buckets = shared_state.num_buckets;
+    COUNTER_SET(ADD_COUNTER(custom_profile(), "NumBuckets", TUnit::UNIT),
+                static_cast<int64_t>(_num_buckets));
+    COUNTER_SET(ADD_COUNTER(custom_profile(), "NumSinkInstances", TUnit::UNIT),
+                static_cast<int64_t>(shared_state.num_sink_instances));
 
     // Now safe to access per_instance_data since init_instances has been 
called.
     auto& inst = shared_state.per_instance_data[_instance_idx];
@@ -117,13 +129,13 @@ Status BucketedAggSinkLocalState::open(RuntimeState* 
state) {
         _aggregate_evaluators.push_back(evaluator->clone(state, p._pool));
     }
 
-    // Set up _bucket_agg_data as 256 pointers into this instance's bucket 
hash tables.
-    _bucket_agg_data.resize(BUCKETED_AGG_NUM_BUCKETS);
-    for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+    // Set up _bucket_agg_data as pointers into this instance's bucket hash 
tables.
+    _bucket_agg_data.resize(_num_buckets);
+    for (int b = 0; b < _num_buckets; ++b) {
         _bucket_agg_data[b] = inst.bucket_agg_data[b].get();
     }
 
-    // Initialize hash method for all 256 bucket hash tables. Each bucket uses
+    // Initialize hash method for all bucket hash tables. Each bucket uses
     // the same hash key type. We use per-instance _probe_expr_ctxs here 
(read-only
     // for data types), which is safe since they were cloned above.
     RETURN_IF_ERROR(_init_hash_method(_probe_expr_ctxs));
@@ -183,7 +195,8 @@ Status 
BucketedAggSinkLocalState::_execute_with_serialized_key(Block* block) {
     }
 
     auto rows = (uint32_t)block->rows();
-    if (_places.size() < rows) {
+    // Inline COUNT updates mapped counters and never reads the places array.
+    if (!shared_state.use_simple_count && _places.size() < rows) {
         _places.resize(rows);
     }
 
@@ -205,7 +218,7 @@ void 
BucketedAggSinkLocalState::_emplace_into_hash_table(AggregateDataPtr* place
     auto& p = Base::_parent->template cast<BucketedAggSinkOperatorX>();
 
     // Use bucket 0's method to compute keys and hash values for all rows.
-    // All 256 buckets use the same hash key type, so the hash function is 
identical.
+    // All buckets use the same hash key type, so the hash function is 
identical.
     std::visit(
             Overload {[&](std::monostate& arg) -> void {
                           throw doris::Exception(ErrorCode::INTERNAL_ERROR, 
"uninited hash table");
@@ -256,23 +269,37 @@ void 
BucketedAggSinkLocalState::_emplace_into_hash_table(AggregateDataPtr* place
                           {
                               SCOPED_TIMER(_hash_table_emplace_timer);
 
-                              // Phase 1: Pre-group rows by bucket index.
-                              // Single pass over hash_values to partition row 
indices
-                              // into 256 bucket groups. This converts random 
256-way scatter
-                              // into sequential per-bucket batches with good 
cache locality.
-                              for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; 
++b) {
-                                  _bucket_row_indices[b].clear();
+                              // A single bucket can consume the whole batch 
without scattering.
+                              if (_num_buckets == 1) {
+                                  lazy_emplace_batch(agg_method, state, 
num_rows, creator,
+                                                     creator_for_null_key,
+                                                     [&](uint32_t row, auto& 
mapped) {
+                                                         if (use_simple_count) 
{
+                                                             
++reinterpret_cast<UInt64&>(mapped);
+                                                         } else {
+                                                             places[row] = 
mapped;
+                                                         }
+                                                     });
+                                  COUNTER_UPDATE(_hash_table_input_counter, 
num_rows);
+                                  return;
                               }
-                              for (uint32_t i = 0; i < num_rows; ++i) {
-                                  int bucket = 
static_cast<int>((agg_method.hash_values[i] >> 24) &
-                                                                0xFF);
-                                  _bucket_row_indices[bucket].push_back(i);
+
+                              // Phase 1: Group rows by bucket to turn random 
scatter into sequential batches.
+                              {
+                                  SCOPED_TIMER(_partition_timer);
+                                  for (int b = 0; b < _num_buckets; ++b) {
+                                      _bucket_row_indices[b].clear();
+                                  }
+                                  for (uint32_t i = 0; i < num_rows; ++i) {
+                                      int bucket =
+                                              
static_cast<int>(BucketedAggSharedState::bucket_index(
+                                                      
agg_method.hash_values[i], _num_buckets));
+                                      _bucket_row_indices[bucket].push_back(i);
+                                  }
                               }
 
-                              // Phase 2: Process each bucket's rows as a 
batch.
-                              // All rows in a bucket hit the same hash table, 
enabling
-                              // effective prefetch and cache-friendly access 
patterns.
-                              for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; 
++b) {
+                              // Phase 2: Process each bucket's rows as a 
batch for cache locality and prefetch.
+                              for (int b = 0; b < _num_buckets; ++b) {
                                   auto& indices = _bucket_row_indices[b];
                                   if (indices.empty()) {
                                       continue;
@@ -282,9 +309,7 @@ void 
BucketedAggSinkLocalState::_emplace_into_hash_table(AggregateDataPtr* place
                                           _bucket_agg_data[b]->method_variant);
                                   auto& bucket_ht = *bucket_method.hash_table;
 
-                                  // Batch emplace with prefetch into this 
bucket's hash table.
                                   for (size_t j = 0; j < indices.size(); ++j) {
-                                      // Prefetch ahead within this bucket's 
hash table.
                                       if (j + HASH_MAP_PREFETCH_DIST < 
indices.size()) {
                                           uint32_t prefetch_row =
                                                   indices[j + 
HASH_MAP_PREFETCH_DIST];
@@ -313,7 +338,7 @@ void 
BucketedAggSinkLocalState::_emplace_into_hash_table(AggregateDataPtr* place
 
 size_t BucketedAggSinkLocalState::_get_hash_table_size() const {
     size_t total = 0;
-    for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+    for (int b = 0; b < _num_buckets; ++b) {
         total += std::visit(Overload {[&](std::monostate& arg) -> size_t { 
return 0; },
                                       [&](auto& agg_method) -> size_t {
                                           return agg_method.hash_table->size();
@@ -324,9 +349,9 @@ size_t BucketedAggSinkLocalState::_get_hash_table_size() 
const {
 }
 
 Status BucketedAggSinkLocalState::_init_hash_method(const VExprContextSPtrs& 
probe_exprs) {
-    // Initialize all 256 bucket hash tables with the same hash key type.
+    // Initialize all bucket hash tables with the same hash key type.
     auto data_types = get_data_types(probe_exprs);
-    for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+    for (int b = 0; b < _num_buckets; ++b) {
         
RETURN_IF_ERROR(init_hash_method<BucketedAggDataVariants>(_bucket_agg_data[b], 
data_types,
                                                                   true /* 
is_first_phase */));
     }
@@ -335,7 +360,7 @@ Status BucketedAggSinkLocalState::_init_hash_method(const 
VExprContextSPtrs& pro
 
 void BucketedAggSinkLocalState::_update_memusage() {
     int64_t hash_table_memory_usage = 0;
-    for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+    for (int b = 0; b < _num_buckets; ++b) {
         std::visit(Overload {[&](std::monostate& arg) -> void {},
                              [&](auto& agg_method) -> void {
                                  hash_table_memory_usage +=
@@ -351,23 +376,15 @@ void BucketedAggSinkLocalState::_update_memusage() {
 }
 
 size_t BucketedAggSinkLocalState::get_reserve_mem_size(RuntimeState* state, 
bool eos) const {
-    // Estimate the memory needed for the next batch across all 256 bucket 
hash tables.
-    // Each bucket's hash table may need to resize, so we take the max 
estimate across
-    // all buckets and multiply conservatively. In practice, data is 
distributed across
-    // buckets, so the per-bucket growth is batch_size/256 on average, but we 
use the
-    // full batch_size estimate from the largest bucket for safety.
+    // Sum the growth estimates for all active buckets. Allow twice the average
+    // batch contribution per bucket, capped at the full batch for a single 
bucket.
     size_t size_to_reserve = 0;
-    for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+    for (int b = 0; b < _num_buckets; ++b) {
         size_to_reserve += std::visit(
                 Overload {[&](std::monostate& arg) -> size_t { return 0; },
                           [&](auto& agg_method) -> size_t {
-                              // estimate_memory returns the delta needed if 
we insert
-                              // num_elem more entries. Since rows are 
distributed across
-                              // 256 buckets, each bucket sees ~batch_size/256 
rows on average.
-                              // However, skew is possible, so we use a 
conservative per-bucket
-                              // estimate of batch_size/128 (2x average).
                               auto per_bucket_rows = std::max<size_t>(
-                                      1, state->batch_size() / 
(BUCKETED_AGG_NUM_BUCKETS / 2));
+                                      1, state->batch_size() / std::max(1, 
_num_buckets / 2));
                               return 
agg_method.hash_table->estimate_memory(per_bucket_rows);
                           }},
                 _bucket_agg_data[b]->method_variant);
@@ -408,6 +425,10 @@ bool BucketedAggSinkOperatorX::has_blockable_aggregate() 
const {
 Status BucketedAggSinkOperatorX::init(const TPlanNode& tnode, RuntimeState* 
state) {
     RETURN_IF_ERROR(DataSinkOperatorX<BucketedAggSinkLocalState>::init(tnode, 
state));
 
+    if (tnode.bucketed_agg_node.__isset.num_buckets) {
+        _num_buckets = tnode.bucketed_agg_node.num_buckets;
+    }
+
     RETURN_IF_ERROR(
             VExpr::create_expr_trees(tnode.bucketed_agg_node.grouping_exprs, 
_probe_expr_ctxs));
 
diff --git a/be/src/exec/operator/bucketed_aggregation_sink_operator.h 
b/be/src/exec/operator/bucketed_aggregation_sink_operator.h
index 1cd6a16fbe6..6096f2e1963 100644
--- a/be/src/exec/operator/bucketed_aggregation_sink_operator.h
+++ b/be/src/exec/operator/bucketed_aggregation_sink_operator.h
@@ -29,7 +29,7 @@ namespace doris {
 class BucketedAggSinkOperatorX;
 
 /// Sink-side local state for bucketed hash aggregation.
-/// Each pipeline instance builds 256 per-bucket hash tables (two-level hash 
table).
+/// Each pipeline instance builds a configurable number of per-bucket hash 
tables.
 /// No locking: each instance writes to per_instance_data[_instance_idx].
 class BucketedAggSinkLocalState : public 
PipelineXSinkLocalState<BucketedAggSharedState> {
 public:
@@ -59,8 +59,9 @@ private:
     void _update_memusage();
 
     int _instance_idx = 0;
+    int _num_buckets = 0;
     /// Pointers into shared_state->per_instance_data[_instance_idx].
-    std::vector<BucketedAggDataVariants*> _bucket_agg_data; // [256]
+    std::vector<BucketedAggDataVariants*> _bucket_agg_data;
     Arena* _arena = nullptr;
 
     /// Per-instance clones of probe expression contexts. Required because
@@ -83,6 +84,7 @@ private:
 
     RuntimeProfile::Counter* _hash_table_compute_timer = nullptr;
     RuntimeProfile::Counter* _hash_table_emplace_timer = nullptr;
+    RuntimeProfile::Counter* _partition_timer = nullptr;
     RuntimeProfile::Counter* _hash_table_input_counter = nullptr;
     RuntimeProfile::Counter* _build_timer = nullptr;
     RuntimeProfile::Counter* _expr_timer = nullptr;
@@ -98,7 +100,7 @@ private:
 
 /// Bucketed hash aggregation sink operator.
 /// Fuses local + global aggregation for single-BE deployments.
-/// Each pipeline instance builds 256 per-bucket hash tables from raw input.
+/// Each pipeline instance builds per-bucket hash tables from raw input.
 /// The source operator then merges across instances per-bucket (second-phase 
agg).
 class BucketedAggSinkOperatorX final : public 
DataSinkOperatorX<BucketedAggSinkLocalState> {
 public:
@@ -136,6 +138,8 @@ public:
 private:
     friend class BucketedAggSinkLocalState;
 
+    // Missing Thrift field preserves the behavior of an older FE. Zero 
selects auto.
+    int _num_buckets = BUCKETED_AGG_NUM_BUCKETS;
     std::vector<AggFnEvaluator*> _aggregate_evaluators;
 
     // Bucketed agg is one-phase: intermediate and output tuples are identical.
diff --git a/be/src/exec/operator/bucketed_aggregation_source_operator.cpp 
b/be/src/exec/operator/bucketed_aggregation_source_operator.cpp
index 44221f14a4c..b1d20022bd8 100644
--- a/be/src/exec/operator/bucketed_aggregation_source_operator.cpp
+++ b/be/src/exec/operator/bucketed_aggregation_source_operator.cpp
@@ -217,6 +217,11 @@ void BucketedAggLocalState::_wake_up_other_sources() {
 int BucketedAggLocalState::_merge_bucket(int bucket, int merge_target) {
     SCOPED_TIMER(_merge_timer);
     auto& shared_state = *_shared_state;
+    // The sole sink already owns the final states. Source-side bucket 
ownership
+    // still coordinates output when the number of source tasks differs.
+    if (shared_state.num_sink_instances == 1) {
+        return 0;
+    }
     auto& bs = shared_state.bucket_states[bucket];
     // Other source instances may merge other buckets at the same time, so 
aggregate
     // function merges must allocate from this source instance's own arena.
@@ -539,7 +544,7 @@ Status 
BucketedAggLocalState::_merge_and_output_null_keys(RuntimeState* state, B
     size_t key_size = shared_state.probe_expr_ctxs.size();
     int merge_target = 
shared_state.merge_target_instance.load(std::memory_order_relaxed);
 
-    // Merge null keys from all 256 buckets (in merge target) into bucket 0.
+    // Merge null keys from all buckets (in merge target) into bucket 0.
     // After per-bucket merge, each bucket in merge target may have its own 
null key data.
     // We merge them all into bucket 0's null key.
     auto& target_inst = shared_state.per_instance_data[merge_target];
@@ -555,8 +560,8 @@ Status 
BucketedAggLocalState::_merge_and_output_null_keys(RuntimeState* state, B
                              if constexpr (has_nullable_key_v<
                                                    
std::remove_reference_t<decltype(dst_data)>>) {
                                  const bool use_simple_count = 
shared_state.use_simple_count;
-                                 // Merge null keys from buckets [1..255] into 
bucket 0.
-                                 for (int b = 1; b < BUCKETED_AGG_NUM_BUCKETS; 
++b) {
+                                 // Merge null keys from the other active 
buckets into bucket 0.
+                                 for (int b = 1; b < shared_state.num_buckets; 
++b) {
                                      auto& src_method = 
std::get<AggMethodType>(
                                              
target_inst.bucket_agg_data[b]->method_variant);
                                      auto& src_data = *src_method.hash_table;
@@ -654,7 +659,7 @@ Status BucketedAggLocalState::_get_results(RuntimeState* 
state, Block* block, bo
                           shared_state.num_sink_instances;
     bool did_work = false;
 
-    for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+    for (int b = 0; b < shared_state.num_buckets; ++b) {
         auto& bs = shared_state.bucket_states[b];
 
         // Skip buckets already fully output.
@@ -713,15 +718,15 @@ Status BucketedAggLocalState::_get_results(RuntimeState* 
state, Block* block, bo
         }
     }
 
-    // Check if all 256 buckets have been output.
+    // Check if all buckets have been output.
     {
         int actually_done = 0;
-        for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+        for (int b = 0; b < shared_state.num_buckets; ++b) {
             if 
(shared_state.bucket_states[b].output_done.load(std::memory_order_acquire)) {
                 ++actually_done;
             }
         }
-        if (actually_done == BUCKETED_AGG_NUM_BUCKETS) {
+        if (actually_done == shared_state.num_buckets) {
             // All buckets output. Handle null key.
             bool expected = false;
             if 
(shared_state.null_key_output_claimed.compare_exchange_strong(expected, true)) {
diff --git a/be/src/exec/pipeline/dependency.cpp 
b/be/src/exec/pipeline/dependency.cpp
index 85ca4f0c21b..2254fecced3 100644
--- a/be/src/exec/pipeline/dependency.cpp
+++ b/be/src/exec/pipeline/dependency.cpp
@@ -249,8 +249,9 @@ void AggSharedState::_close_without_key() {
     }
 }
 
-BucketedAggSharedState::PerInstanceData::PerInstanceData() : 
arena(std::make_unique<Arena>()) {
-    bucket_agg_data.resize(BUCKETED_AGG_NUM_BUCKETS);
+BucketedAggSharedState::PerInstanceData::PerInstanceData(int num_buckets)
+        : arena(std::make_unique<Arena>()) {
+    bucket_agg_data.resize(num_buckets);
     for (auto& p : bucket_agg_data) {
         p = std::make_unique<BucketedAggDataVariants>();
     }
@@ -261,16 +262,36 @@ BucketedAggSharedState::~BucketedAggSharedState() {
 }
 
 Status BucketedAggSharedState::init_instances(int num_instances,
-                                              const std::function<Status()>& 
metadata_init) {
+                                              const std::function<Status()>& 
metadata_init,
+                                              int requested_num_buckets) {
     std::call_once(_init_once, [&]() {
+        // Mask-based routing requires a power of two, and the merge state has 
256 slots.
+        if (requested_num_buckets < 0 || requested_num_buckets > 
BUCKETED_AGG_NUM_BUCKETS ||
+            (requested_num_buckets & (requested_num_buckets - 1)) != 0) {
+            _init_status = Status::InvalidArgument(
+                    "Bucketed aggregation bucket count must be 0 or a power of 
two in [1, {}], got "
+                    "{}",
+                    BUCKETED_AGG_NUM_BUCKETS, requested_num_buckets);
+            return;
+        }
+        // One sink can feed several sources. Keep multiple buckets in that 
case,
+        // otherwise one source holds the only bucket lock across every output 
batch.
+        num_buckets = requested_num_buckets;
+        if (num_buckets == 0) {
+            num_buckets =
+                    num_instances == 1 && source_deps.size() == 1 ? 1 : 
BUCKETED_AGG_NUM_BUCKETS;
+        }
         num_sink_instances = num_instances;
-        per_instance_data.resize(num_instances);
-        sink_finished = std::make_unique<std::atomic<bool>[]>(num_instances);
+        per_instance_data.reserve(num_instances);
+        for (int i = 0; i < num_instances; ++i) {
+            per_instance_data.emplace_back(num_buckets);
+        }
+        sink_finished = 
std::make_unique<std::atomic<bool>[]>(per_instance_data.size());
         for (int i = 0; i < num_instances; ++i) {
             sink_finished[i].store(false, std::memory_order_relaxed);
         }
-        for (auto& bs : bucket_states) {
-            bs.merged_instances.resize(num_instances, false);
+        for (int b = 0; b < num_buckets; ++b) {
+            bucket_states[b].merged_instances.resize(num_instances, false);
         }
         source_merge_arenas.resize(source_deps.size());
         for (auto& arena : source_merge_arenas) {
diff --git a/be/src/exec/pipeline/dependency.h 
b/be/src/exec/pipeline/dependency.h
index 714b9c10a52..63f0bf50a1a 100644
--- a/be/src/exec/pipeline/dependency.h
+++ b/be/src/exec/pipeline/dependency.h
@@ -416,9 +416,8 @@ static constexpr int BUCKETED_AGG_NUM_BUCKETS = 256;
 
 /// Shared state for BucketedAggSinkOperatorX / BucketedAggSourceOperatorX.
 ///
-/// Each sink pipeline instance owns 256 per-bucket hash tables (two-level 
hash table
-/// approach, inspired by ClickHouse). During sink, each row is routed to 
bucket
-/// (hash >> 24) & 0xFF.
+/// Each sink pipeline instance owns num_buckets hash tables. Bucket selection 
uses
+/// the high bits of the mixed hash, independently of the low bits used by 
phmap.
 ///
 /// Source-side merge is pipelined with sink completion: as each sink instance 
finishes,
 /// it unblocks all source dependencies. Source instances scan buckets and 
merge data
@@ -440,14 +439,14 @@ public:
     ~BucketedAggSharedState() override; // defined in dependency.cpp (variant 
machinery)
 
     /// Per-instance data. One per sink pipeline instance.
-    /// Each instance has 256 bucket hash tables + 1 shared arena.
+    /// Each instance has num_buckets hash tables + 1 shared arena.
     struct PerInstanceData {
-        /// 256 per-bucket hash tables. Each bucket has its own 
BucketedAggDataVariants.
+        /// Each bucket has its own BucketedAggDataVariants.
         /// Uses PHHashMap<StringRef> for string keys instead of StringHashMap.
         std::vector<std::unique_ptr<BucketedAggDataVariants>> bucket_agg_data;
         std::unique_ptr<Arena> arena;
 
-        PerInstanceData(); // defined in dependency.cpp (variant machinery)
+        explicit PerInstanceData(int num_buckets); // defined in dependency.cpp
     };
 
     /// Per-bucket merge state for pipelined source-side processing.
@@ -465,6 +464,15 @@ public:
 
     std::vector<PerInstanceData> per_instance_data;
     int num_sink_instances = 0;
+    int num_buckets = BUCKETED_AGG_NUM_BUCKETS;
+
+    /// phmap uses hash bits [7, ...] for the initial probe and [0, 6] for its
+    /// fingerprint. Using bits [24, 31] for routing fixes an index bit once a
+    /// bucket reaches capacity 2^18, clustering probes in half the table.
+    /// The hash has already been mixed by phmap, including for CRC32 hashers.
+    static size_t bucket_index(size_t hash, int num_buckets) {
+        return (hash >> (sizeof(size_t) * 8 - 8)) & (num_buckets - 1);
+    }
 
     /// Tracks how many sinks have finished. Incremented by each sink on EOS.
     std::atomic<int> num_sinks_finished = 0;
@@ -526,7 +534,8 @@ public:
     /// Once-per-query cold path; defined in dependency.cpp so the body (which
     /// materializes the per-bucket BucketedAggDataVariants storage) stays out
     /// of every includer's parse.
-    Status init_instances(int num_instances, const std::function<Status()>& 
metadata_init);
+    Status init_instances(int num_instances, const std::function<Status()>& 
metadata_init,
+                          int requested_num_buckets = 
BUCKETED_AGG_NUM_BUCKETS);
 
 private:
     std::once_flag _init_once;
diff --git a/be/test/exec/operator/agg_operator_test.cpp 
b/be/test/exec/operator/agg_operator_test.cpp
index f8e866eb4ce..35fdd05cc34 100644
--- a/be/test/exec/operator/agg_operator_test.cpp
+++ b/be/test/exec/operator/agg_operator_test.cpp
@@ -389,7 +389,9 @@ TEST_F(BucketedAggOperatorTest, merges_two_sinks) {
 // still destroys it when the failed query is torn down.
 TEST_F(BucketedAggOperatorTest, 
failed_source_merge_keeps_every_state_for_cleanup) {
     ASSERT_TRUE(sink(0, {1, 2, 3}, true).ok());
-    ASSERT_TRUE(sink(1, {1, 2, 4}, true).ok());
+    // Every non-empty bucket must merge a duplicate before get_block can 
return output,
+    // independently of the bucket routing and traversal order.
+    ASSERT_TRUE(sink(1, {1, 2, 3}, true).ok());
     ASSERT_EQ(count_live_states(), 6);
 
     DebugPoints::instance()->add(FAIL_MERGE_DEBUG_POINT);
diff --git a/be/test/exec/pipeline/bucketed_agg_shared_state_test.cpp 
b/be/test/exec/pipeline/bucketed_agg_shared_state_test.cpp
index 39ecaba7a67..35370ab06d8 100644
--- a/be/test/exec/pipeline/bucketed_agg_shared_state_test.cpp
+++ b/be/test/exec/pipeline/bucketed_agg_shared_state_test.cpp
@@ -17,6 +17,8 @@
 
 #include <gtest/gtest.h>
 
+#include <thread>
+
 #include "exec/common/agg_utils.h"
 #include "exec/pipeline/dependency.h"
 
@@ -41,4 +43,93 @@ TEST(BucketedAggSharedStateTest, 
InitInstancesCreatesOneMergeArenaPerSource) {
     }
 }
 
+TEST(BucketedAggSharedStateTest, 
BucketRoutingDoesNotConstrainPhmapProbeOrFingerprint) {
+    // At capacity 2^18 - 1, phmap uses bits [7, 24] for its initial probe.
+    // Fixing a bucket must leave both halves of that index space available.
+    // The old (hash >> 24) & 255 routing fails this invariant.
+    for (int num_buckets : {1, 2, 4, 8, 16, 32, 64, 128, 256}) {
+        for (int bucket = 0; bucket < num_buckets; ++bucket) {
+            for (size_t slot : {0UL, (1UL << 17) - 1, 1UL << 17, (1UL << 18) - 
1}) {
+                for (size_t fingerprint : {0UL, 127UL}) {
+                    size_t hash = (size_t(bucket) << 56) | (slot << 7) | 
fingerprint;
+                    EXPECT_EQ(BucketedAggSharedState::bucket_index(hash, 
num_buckets), bucket);
+                    EXPECT_EQ((hash >> 7) & ((1UL << 18) - 1), slot);
+                }
+            }
+        }
+    }
+}
+
+// GTest assertion macros inflate the complexity of this configuration matrix.
+// NOLINTNEXTLINE(readability-function-cognitive-complexity)
+TEST(BucketedAggSharedStateTest, ConfiguredBucketsAndAutomaticSelection) {
+    for (int sinks : {1, 4}) {
+        for (int requested : {0, 1, 2, 4, 8, 16, 32, 64, 128, 256}) {
+            BucketedAggSharedState state;
+            state.create_source_dependencies(3, 0, 0, "BUCKETED_AGG_SOURCE");
+            ASSERT_TRUE(state.init_instances(
+                                     sinks, [] { return Status::OK(); }, 
requested)
+                                .ok());
+            int expected = requested == 0 ? 256 : requested;
+            ASSERT_EQ(state.num_buckets, expected);
+            ASSERT_EQ(state.per_instance_data.size(), sinks);
+            for (const auto& instance : state.per_instance_data) {
+                ASSERT_EQ(instance.bucket_agg_data.size(), expected);
+            }
+            for (int b = 0; b < BUCKETED_AGG_NUM_BUCKETS; ++b) {
+                EXPECT_EQ(state.bucket_states[b].merged_instances.size(), b < 
expected ? sinks : 0);
+            }
+            EXPECT_EQ(state.source_merge_arenas.size(), 3);
+        }
+    }
+    BucketedAggSharedState serial_state;
+    serial_state.create_source_dependencies(1, 0, 0, "BUCKETED_AGG_SOURCE");
+    ASSERT_TRUE(serial_state
+                        .init_instances(
+                                1, [] { return Status::OK(); }, 0)
+                        .ok());
+    EXPECT_EQ(serial_state.num_buckets, 1);
+    EXPECT_EQ(serial_state.per_instance_data[0].bucket_agg_data.size(), 1);
+}
+
+TEST(BucketedAggSharedStateTest, 
ConcurrentInitializationPublishesOneConfiguration) {
+    BucketedAggSharedState state;
+    state.create_source_dependencies(3, 0, 0, "BUCKETED_AGG_SOURCE");
+    std::atomic<int> metadata_calls {0};
+    auto initialize = [&] {
+        EXPECT_TRUE(state.init_instances(
+                                 4,
+                                 [&] {
+                                     ++metadata_calls;
+                                     return Status::OK();
+                                 },
+                                 16)
+                            .ok());
+        EXPECT_EQ(state.num_buckets, 16);
+        EXPECT_EQ(state.per_instance_data.size(), 4);
+        EXPECT_EQ(state.per_instance_data[0].bucket_agg_data.size(), 16);
+    };
+    std::thread first(initialize);
+    std::thread second(initialize);
+    initialize();
+    first.join();
+    second.join();
+    EXPECT_EQ(metadata_calls, 1);
+}
+
+TEST(BucketedAggSharedStateTest, 
InvalidConfigurationIsRejectedBeforeAllocation) {
+    for (int requested : {-1, 3, 255, 257, 512}) {
+        BucketedAggSharedState state;
+        bool metadata_called = false;
+        auto initialize = [&] {
+            metadata_called = true;
+            return Status::OK();
+        };
+        EXPECT_FALSE(state.init_instances(2, initialize, requested).ok());
+        EXPECT_FALSE(state.init_instances(2, initialize, requested).ok());
+        EXPECT_FALSE(metadata_called);
+        EXPECT_TRUE(state.per_instance_data.empty());
+    }
+}
+
 } // namespace doris
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
index 35e271b703a..e648f333560 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
@@ -3491,7 +3491,8 @@ public class PhysicalPlanTranslator extends 
DefaultPlanVisitor<PlanFragment, Pla
                 AggregateInfo.AggPhase.FIRST);
 
         BucketedAggregationNode bucketedAggNode = new BucketedAggregationNode(
-                context.nextPlanNodeId(), inputPlanFragment.getPlanRoot(), 
aggInfo, true);
+                context.nextPlanNodeId(), inputPlanFragment.getPlanRoot(), 
aggInfo, true,
+                context.getSessionVariable().bucketedAggNumBuckets);
 
         bucketedAggNode.setNereidsId(aggregate.getId());
         context.getNereidsIdToPlanNodeIdMap().put(aggregate.getId(), 
bucketedAggNode.getId());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/planner/BucketedAggregationNode.java
 
b/fe/fe-core/src/main/java/org/apache/doris/planner/BucketedAggregationNode.java
index 5c36ead71fe..354e354e97d 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/planner/BucketedAggregationNode.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/planner/BucketedAggregationNode.java
@@ -40,12 +40,14 @@ import java.util.List;
 public class BucketedAggregationNode extends PlanNode {
     private final AggregateInfo aggInfo;
     private final boolean needsFinalize;
+    private final int numBuckets;
 
     public BucketedAggregationNode(PlanNodeId id, PlanNode input, 
AggregateInfo aggInfo,
-            boolean needsFinalize) {
+            boolean needsFinalize, int numBuckets) {
         super(id, aggInfo.getOutputTupleId().asList(), "BUCKETED AGGREGATE");
         this.aggInfo = aggInfo;
         this.needsFinalize = needsFinalize;
+        this.numBuckets = numBuckets;
         this.children.add(input);
     }
 
@@ -68,6 +70,7 @@ public class BucketedAggregationNode extends PlanNode {
         bucketedAggNode.setAggregateFunctions(aggregateFunctions);
         bucketedAggNode.setTupleId(aggInfo.getOutputTupleId().asInt());
         bucketedAggNode.setNeedFinalize(needsFinalize);
+        bucketedAggNode.setNumBuckets(numBuckets);
 
         msg.bucketed_agg_node = bucketedAggNode;
     }
@@ -76,6 +79,8 @@ public class BucketedAggregationNode extends PlanNode {
     public String getNodeExplainString(String detailPrefix, TExplainLevel 
detailLevel) {
         StringBuilder output = new StringBuilder();
         output.append(detailPrefix).append("BUCKETED").append("\n");
+        output.append(detailPrefix).append("buckets: ")
+                .append(numBuckets == 0 ? "auto" : 
Integer.toString(numBuckets)).append("\n");
 
         if (detailLevel == TExplainLevel.BRIEF) {
             
output.append(detailPrefix).append(String.format("cardinality=%,d", 
cardinality)).append("\n");
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java 
b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
index f16f9639e13..637fdf5793c 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
@@ -1044,6 +1044,7 @@ public class SessionVariable implements Serializable, 
Writable {
     public static final String MULTI_DISTINCT_STRATEGY = 
"multi_distinct_strategy";
     public static final String AGG_PHASE = "agg_phase";
     public static final String ENABLE_BUCKETED_HASH_AGG = 
"enable_bucketed_hash_agg";
+    public static final String BUCKETED_AGG_NUM_BUCKETS = 
"bucketed_agg_num_buckets";
     public static final String BUCKETED_AGG_MIN_INPUT_ROWS = 
"bucketed_agg_min_input_rows";
     public static final String BUCKETED_AGG_MAX_GROUP_KEYS = 
"bucketed_agg_max_group_keys";
     public static final String BUCKETED_AGG_HIGH_CARD_THRESHOLD = 
"bucketed_agg_high_card_threshold";
@@ -3113,9 +3114,16 @@ public class SessionVariable implements Serializable, 
Writable {
             + "and serialization/deserialization costs. Enabled by default.")
     public boolean enableBucketedHashAgg = true;
 
+    @VarAttrDef.VarAttr(name = BUCKETED_AGG_NUM_BUCKETS, needForward = true,
+            checker = "checkBucketedAggNumBuckets",
+            description = "Number of hash buckets used by bucketed 
aggregation. "
+            + "Must be 0 (automatic) or a power of two between 1 and 256. 
Automatic uses one bucket when both "
+            + "sink and source have one instance on a BE, and 256 buckets 
otherwise. Default 0.")
+    public int bucketedAggNumBuckets = 0;
+
     @VarAttrDef.VarAttr(name = BUCKETED_AGG_MIN_INPUT_ROWS, fuzzy = true, 
needForward = true, description = "Minimum "
             + "estimated input rows required for bucketed hash aggregation. 
When estimated input "
-            + "rows are below this threshold, the data volume is too small for 
the 256-bucket two-level "
+            + "rows are below this threshold, the data volume is too small for 
the bucketed "
             + "hash table overhead to be worthwhile. Set to 0 to disable this 
check. Default 100000.")
     public long bucketedAggMinInputRows = 100000;
 
@@ -6464,6 +6472,19 @@ public class SessionVariable implements Serializable, 
Writable {
         }
     }
 
+    public void checkBucketedAggNumBuckets(String numBucketsStr) {
+        try {
+            int numBuckets = Integer.parseInt(numBucketsStr);
+            if (numBuckets < 0 || numBuckets > 256 || (numBuckets & 
(numBuckets - 1)) != 0) {
+                throw new InvalidParameterException(
+                        BUCKETED_AGG_NUM_BUCKETS + " must be 0 or a power of 
two between 1 and 256");
+            }
+        } catch (NumberFormatException e) {
+            throw new InvalidParameterException(
+                    BUCKETED_AGG_NUM_BUCKETS + " must be 0 or a power of two 
between 1 and 256");
+        }
+    }
+
     public void checkAggPhase(String aggPhaseStr) {
         try {
             long aggPhase = Long.parseLong(aggPhaseStr);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/BucketedAggregateTranslatorTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/BucketedAggregateTranslatorTest.java
index 40b241a0b1a..760ef0f83ca 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/BucketedAggregateTranslatorTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/BucketedAggregateTranslatorTest.java
@@ -30,6 +30,7 @@ import org.apache.doris.planner.PlanFragment;
 import org.apache.doris.planner.PlanNode;
 import org.apache.doris.planner.Planner;
 import org.apache.doris.qe.SessionVariable;
+import org.apache.doris.thrift.TBucketedAggregationNode;
 import org.apache.doris.thrift.TExplainLevel;
 import org.apache.doris.utframe.TestWithFeService;
 
@@ -684,6 +685,53 @@ public class BucketedAggregateTranslatorTest extends 
TestWithFeService {
                 .collect(Collectors.joining("\n"));
     }
 
+    @Test
+    public void testConfiguredBucketCountIsCapturedInPlan() throws Exception {
+        SessionVariable sessionVariable = connectContext.getSessionVariable();
+        int oldAggPhase = sessionVariable.aggPhase;
+        int oldBeNumberForTest = sessionVariable.getBeNumberForTest();
+        int oldParallelPipelineTaskNum = 
sessionVariable.parallelPipelineTaskNum;
+        int oldBucketedAggNumBuckets = sessionVariable.bucketedAggNumBuckets;
+        long oldBucketedAggMinInputRows = 
sessionVariable.bucketedAggMinInputRows;
+        long oldBucketedAggMaxGroupKeys = 
sessionVariable.bucketedAggMaxGroupKeys;
+        double oldBucketedAggHighCardThreshold = 
sessionVariable.bucketedAggHighCardThreshold;
+        boolean oldEnableBucketedHashAgg = 
sessionVariable.enableBucketedHashAgg;
+        try {
+            sessionVariable.aggPhase = 1;
+            sessionVariable.setBeNumberForTest(1);
+            sessionVariable.bucketedAggMinInputRows = 0;
+            sessionVariable.bucketedAggMaxGroupKeys = 0;
+            sessionVariable.bucketedAggHighCardThreshold = 1.0;
+            sessionVariable.enableBucketedHashAgg = true;
+            for (int parallelism : new int[] {1, 4}) {
+                sessionVariable.parallelPipelineTaskNum = parallelism;
+                for (int numBuckets : new int[] {0, 1, 16, 256}) {
+                    sessionVariable.bucketedAggNumBuckets = numBuckets;
+                    List<BucketedAggregationNode> nodes = 
collectBucketedAggregationNodes("sum(kint)");
+                    Assertions.assertEquals(1, nodes.size());
+                    BucketedAggregationNode node = nodes.get(0);
+                    // Serialization must use the plan's captured value, even 
if the session changes later.
+                    sessionVariable.bucketedAggNumBuckets = numBuckets == 256 
? 0 : 256;
+                    TBucketedAggregationNode thriftNode = 
node.treeToThrift().getNodes().get(0).getBucketedAggNode();
+                    Assertions.assertTrue(thriftNode.isSetNumBuckets());
+                    Assertions.assertEquals(numBuckets, 
thriftNode.getNumBuckets());
+                    String expected = "buckets: " + (numBuckets == 0 ? "auto" 
: Integer.toString(numBuckets)) + "\n";
+                    Assertions.assertTrue(node.getNodeExplainString("", 
TExplainLevel.NORMAL).contains(expected));
+                    Assertions.assertTrue(node.getNodeExplainString("", 
TExplainLevel.BRIEF).contains(expected));
+                }
+            }
+        } finally {
+            sessionVariable.aggPhase = oldAggPhase;
+            sessionVariable.setBeNumberForTest(oldBeNumberForTest);
+            sessionVariable.parallelPipelineTaskNum = 
oldParallelPipelineTaskNum;
+            sessionVariable.bucketedAggNumBuckets = oldBucketedAggNumBuckets;
+            sessionVariable.bucketedAggMinInputRows = 
oldBucketedAggMinInputRows;
+            sessionVariable.bucketedAggMaxGroupKeys = 
oldBucketedAggMaxGroupKeys;
+            sessionVariable.bucketedAggHighCardThreshold = 
oldBucketedAggHighCardThreshold;
+            sessionVariable.enableBucketedHashAgg = oldEnableBucketedHashAgg;
+        }
+    }
+
     private void assertUsesRegularAggregation(String aggregateFunction) throws 
Exception {
         Planner planner = planAggregate(aggregateFunction);
         Assertions.assertTrue(collectNodes(planner, 
BucketedAggregationNode.class).isEmpty());
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeWhereSubqueryTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeWhereSubqueryTest.java
index 0695b05ef70..bc8748ea7c8 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeWhereSubqueryTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeWhereSubqueryTest.java
@@ -124,12 +124,13 @@ public class AnalyzeWhereSubqueryTest extends 
TestWithFeService implements MemoP
             try {
                 StatementScopeIdGenerator.clear();
                 StatementContext statementContext = 
MemoTestUtils.createStatementContext(connectContext, sql);
-                PhysicalPlan plan = new 
NereidsPlanner(statementContext).planWithLock(
+                NereidsPlanner planner = new NereidsPlanner(statementContext);
+                PhysicalPlan plan = planner.planWithLock(
                         parser.parseSingle(sql),
                         PhysicalProperties.ANY
                 );
                 // Just to check whether translate will throw exception
-                new PhysicalPlanTranslator(new 
PlanTranslatorContext()).translatePlan(plan);
+                new PhysicalPlanTranslator(new 
PlanTranslatorContext(planner.getCascadesContext())).translatePlan(plan);
             } catch (Throwable t) {
                 throw new IllegalStateException("Test sql failed: " + 
t.getMessage() + ", sql:\n" + sql, t);
             }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/qe/SessionVariableBucketedAggTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/qe/SessionVariableBucketedAggTest.java
new file mode 100644
index 00000000000..864fe250fac
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/qe/SessionVariableBucketedAggTest.java
@@ -0,0 +1,65 @@
+// 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.
+
+package org.apache.doris.qe;
+
+import org.apache.doris.analysis.SetType;
+import org.apache.doris.analysis.SetVar;
+import org.apache.doris.analysis.StringLiteral;
+import org.apache.doris.common.DdlException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class SessionVariableBucketedAggTest {
+
+    @Test
+    void testDefaultAndForwarding() throws Exception {
+        SessionVariable sessionVariable = new SessionVariable();
+        Assertions.assertEquals(0, sessionVariable.bucketedAggNumBuckets);
+        VariableMgr.setVar(sessionVariable, new SetVar(SetType.SESSION,
+                SessionVariable.BUCKETED_AGG_NUM_BUCKETS, new 
StringLiteral("16")));
+
+        SessionVariable forwarded = new SessionVariable();
+        
forwarded.setForwardedSessionVariables(sessionVariable.getForwardVariables());
+        Assertions.assertEquals(16, forwarded.bucketedAggNumBuckets);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"0", "1", "2", "4", "8", "16", "32", "64", "128", 
"256"})
+    void testAcceptsAutomaticAndPowersOfTwo(String value) throws Exception {
+        SessionVariable sessionVariable = new SessionVariable();
+        VariableMgr.setVar(sessionVariable, new SetVar(SetType.SESSION,
+                SessionVariable.BUCKETED_AGG_NUM_BUCKETS, new 
StringLiteral(value)));
+        Assertions.assertEquals(Integer.parseInt(value), 
sessionVariable.bucketedAggNumBuckets);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"-1", "-2147483648", "3", "7", "255", "257", 
"512", "2147483648", "1.5", "auto", ""})
+    void testRejectsInvalidBucketCounts(String value) {
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.bucketedAggNumBuckets = 16;
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() ->
+                VariableMgr.setVar(sessionVariable, new SetVar(SetType.SESSION,
+                        SessionVariable.BUCKETED_AGG_NUM_BUCKETS, new 
StringLiteral(value))));
+        Assertions.assertTrue(exception.getMessage().contains(
+                "bucketed_agg_num_buckets must be 0 or a power of two between 
1 and 256"));
+        Assertions.assertEquals(16, sessionVariable.bucketedAggNumBuckets);
+    }
+}
diff --git a/gensrc/thrift/PlanNodes.thrift b/gensrc/thrift/PlanNodes.thrift
index 1cb49705c5b..4a84b7ed955 100644
--- a/gensrc/thrift/PlanNodes.thrift
+++ b/gensrc/thrift/PlanNodes.thrift
@@ -1322,6 +1322,9 @@ struct TBucketedAggregationNode {
   // so intermediate and output tuples are always identical.
   3: optional Types.TTupleId tuple_id
   5: optional bool need_finalize
+  // 0 selects one bucket when both sink and source have one instance, 
otherwise 256 buckets.
+  // Positive values must be powers of two in [1, 256]. Missing retains 256 
buckets.
+  6: optional i32 num_buckets
 }
 
 struct TRepeatNode {
diff --git 
a/regression-test/data/nereids_rules_p0/agg_strategy/bucketed_hash_agg_num_buckets.out
 
b/regression-test/data/nereids_rules_p0/agg_strategy/bucketed_hash_agg_num_buckets.out
new file mode 100644
index 00000000000..0ec53f56edb
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/agg_strategy/bucketed_hash_agg_num_buckets.out
@@ -0,0 +1,1236 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !string_1_0 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_1_0 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_1_0 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_1_0 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_1_0 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_1_0 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_1_0 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_1_0 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_1_0 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_1_0 --
+
+-- !string_1_1 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_1_1 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_1_1 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_1_1 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_1_1 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_1_1 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_1_1 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_1_1 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_1_1 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_1_1 --
+
+-- !string_1_4 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_1_4 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_1_4 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_1_4 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_1_4 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_1_4 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_1_4 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_1_4 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_1_4 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_1_4 --
+
+-- !string_1_16 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_1_16 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_1_16 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_1_16 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_1_16 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_1_16 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_1_16 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_1_16 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_1_16 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_1_16 --
+
+-- !string_1_64 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_1_64 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_1_64 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_1_64 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_1_64 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_1_64 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_1_64 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_1_64 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_1_64 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_1_64 --
+
+-- !string_1_256 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_1_256 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_1_256 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_1_256 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_1_256 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_1_256 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_1_256 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_1_256 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_1_256 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_1_256 --
+
+-- !string_4_0 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_4_0 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_4_0 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_4_0 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_4_0 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_4_0 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_4_0 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_4_0 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_4_0 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_4_0 --
+
+-- !string_4_1 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_4_1 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_4_1 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_4_1 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_4_1 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_4_1 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_4_1 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_4_1 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_4_1 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_4_1 --
+
+-- !string_4_4 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_4_4 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_4_4 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_4_4 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_4_4 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_4_4 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_4_4 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_4_4 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_4_4 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_4_4 --
+
+-- !string_4_16 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_4_16 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_4_16 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_4_16 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_4_16 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_4_16 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_4_16 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_4_16 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_4_16 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_4_16 --
+
+-- !string_4_64 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_4_64 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_4_64 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_4_64 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_4_64 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_4_64 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_4_64 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_4_64 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_4_64 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_4_64 --
+
+-- !string_4_256 --
+\N     4096    3276    53660880        8       32752
+0      4096    3277    53693647        7       32767
+1      4096    3277    53693641        1       32761
+2      4096    3277    53693642        2       32762
+3      4096    3277    53693643        3       32763
+4      4096    3277    53693644        4       32764
+5      4096    3276    53660880        12      32758
+6      4096    3277    53693646        6       32766
+
+-- !numeric_4_256 --
+0      2979    39040991
+1      2979    39049920
+10     2978    39032053
+2      2979    39026089
+3      2979    39035028
+4      2979    39043967
+5      2979    39052906
+6      2979    39061840
+7      2979    39038004
+8      2979    39046943
+9      2979    39055882
+
+-- !composite_4_256 --
+0      \N      1366    17886960
+0      0       1365    17886960
+0      1       1365    17886960
+0      2       1365    17886960
+0      3       1366    17919723
+0      4       1365    17886960
+0      5       1365    17886960
+0      6       1366    17919726
+1      \N      1365    17897880
+1      0       1366    17897887
+1      1       1366    17897881
+1      2       1365    17897880
+1      3       1365    17897880
+1      4       1366    17930644
+1      5       1365    17897880
+1      6       1365    17865120
+2      \N      1365    17876040
+2      0       1365    17908800
+2      1       1365    17908800
+2      2       1366    17908802
+2      3       1365    17876040
+2      4       1365    17876040
+2      5       1366    17876040
+2      6       1365    17908800
+
+-- !count_numeric_4_256 --
+0      2979
+1      2979
+10     2978
+2      2979
+3      2979
+4      2979
+5      2979
+6      2979
+7      2979
+8      2979
+9      2979
+
+-- !count_numeric_nullable_4_256 --
+\N     4096
+0      2606
+1      2607
+10     2606
+2      2606
+3      2607
+4      2607
+5      2606
+6      2607
+7      2607
+8      2606
+9      2607
+
+-- !count_string_4_256 --
+0      4682
+1      4681
+2      4681
+3      4681
+4      4681
+5      4681
+6      4681
+
+-- !count_string_nullable_4_256 --
+\N     4096
+0      4096
+1      4096
+2      4096
+3      4096
+4      4096
+5      4096
+6      4096
+
+-- !distinct_numeric_4_256 --
+32768  536854528       32768   1       1
+
+-- !distinct_composite_4_256 --
+32768  536854528       32768   429483623       4096
+
+-- !empty_4_256 --
diff --git 
a/regression-test/suites/nereids_rules_p0/agg_strategy/bucketed_hash_agg_num_buckets.groovy
 
b/regression-test/suites/nereids_rules_p0/agg_strategy/bucketed_hash_agg_num_buckets.groovy
new file mode 100644
index 00000000000..ecef166d1ba
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/agg_strategy/bucketed_hash_agg_num_buckets.groovy
@@ -0,0 +1,177 @@
+// 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("bucketed_hash_agg_num_buckets") {
+    sql "set enable_sql_cache=false"
+    // Bucketed aggregation is disabled while spill is enabled, including in 
fuzzy sessions.
+    sql "set enable_spill=false"
+    sql "set enable_force_spill=false"
+    sql "set enable_bucketed_hash_agg=false"
+    sql "set be_number_for_test=1"
+    sql "set agg_phase=1"
+    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 bucketed_agg_num_buckets_test"
+    sql """
+        CREATE TABLE bucketed_agg_num_buckets_test (
+            id INT NOT NULL,
+            distribution_key INT NOT NULL,
+            numeric_grp INT NOT NULL,
+            nullable_numeric_grp INT,
+            non_null_grp VARCHAR(32) NOT NULL,
+            grp VARCHAR(32),
+            val BIGINT
+        ) DUPLICATE KEY(id, distribution_key)
+        DISTRIBUTED BY HASH(distribution_key) BUCKETS 4
+        PROPERTIES('replication_num'='1')
+    """
+    // The distribution key is absent from every GROUP BY, preserving the 
hash-distribute
+    // shape required by bucketed fusion even for the all-distinct id cases.
+    // Multiple input batches exercise both local table growth and merging 
across sink instances.
+    sql """
+        INSERT INTO bucketed_agg_num_buckets_test
+        SELECT number, number % 13, number % 11, IF(number % 8 = 0, NULL, 
number % 11),
+               CAST(number % 7 AS VARCHAR(32)),
+               IF(number % 8 = 0, NULL, CAST(number % 7 AS VARCHAR(32))),
+               IF(number % 5 = 0, NULL, number)
+        FROM numbers('number'='32768')
+    """
+
+    def queryCases = [
+        string: """
+            SELECT grp, COUNT(*), COUNT(val), SUM(val), MIN(val), MAX(val)
+            FROM bucketed_agg_num_buckets_test GROUP BY grp
+        """,
+        numeric: """
+            SELECT numeric_grp, COUNT(*), SUM(val)
+            FROM bucketed_agg_num_buckets_test GROUP BY numeric_grp
+        """,
+        composite: """
+            SELECT id % 3 AS k, grp, COUNT(*), SUM(val)
+            FROM bucketed_agg_num_buckets_test GROUP BY k, grp
+        """,
+        // COUNT(*) alone selects the inline-count path for supported key 
types.
+        count_numeric: """
+            SELECT numeric_grp, COUNT(*)
+            FROM bucketed_agg_num_buckets_test GROUP BY numeric_grp
+        """,
+        count_numeric_nullable: """
+            SELECT nullable_numeric_grp, COUNT(*)
+            FROM bucketed_agg_num_buckets_test GROUP BY nullable_numeric_grp
+        """,
+        count_string: """
+            SELECT non_null_grp, COUNT(*)
+            FROM bucketed_agg_num_buckets_test GROUP BY non_null_grp
+        """,
+        count_string_nullable: """
+            SELECT grp, COUNT(*)
+            FROM bucketed_agg_num_buckets_test GROUP BY grp
+        """,
+        // Keep 32768 distinct keys in the tested inner aggregate. The outer 
scalar
+        // aggregate reduces expected output to one row and cannot itself be 
bucketed.
+        distinct_numeric: """
+            SELECT COUNT(*), SUM(k), SUM(cnt), MIN(cnt), MAX(cnt)
+            FROM (
+                SELECT id AS k, COUNT(*) AS cnt
+                FROM bucketed_agg_num_buckets_test GROUP BY id
+            ) t
+        """,
+        distinct_composite: """
+            SELECT COUNT(*), SUM(k), SUM(cnt), SUM(total),
+                   SUM(IF(grp IS NULL, cnt, 0))
+            FROM (
+                SELECT id AS k, grp, COUNT(*) AS cnt, SUM(val) AS total
+                FROM bucketed_agg_num_buckets_test GROUP BY id, grp
+            ) t
+        """,
+        // Avoid a constant/range predicate that could remove the aggregate 
from the plan.
+        empty: """
+            SELECT grp, COUNT(*), SUM(val) FROM bucketed_agg_num_buckets_test
+            WHERE MD5(CAST(id AS STRING)) = 'bucketed-empty' GROUP BY grp
+        """
+    ]
+
+    // Compute independent expected rows through regular aggregation once. 
Golden generation
+    // must not accept a shared bucketed-aggregation bug as the expected 
result.
+    sql "set parallel_pipeline_task_num=1"
+    def referenceResults = [:]
+    queryCases.each { kind, query ->
+        explain {
+            sql query
+            notContains "BUCKETED AGGREGATE"
+        }
+        referenceResults[kind] = order_sql(query)
+    }
+    String unlimitedCountQuery = """
+        SELECT id, COUNT(*) FROM bucketed_agg_num_buckets_test GROUP BY id
+    """
+    explain {
+        sql unlimitedCountQuery
+        notContains "BUCKETED AGGREGATE"
+    }
+    def limitReference = order_sql(unlimitedCountQuery).collectEntries { row ->
+        [(row[0]): row[1]]
+    }
+    sql "set enable_bucketed_hash_agg=true"
+
+    for (def parallelism : [1, 4]) {
+        sql "set parallel_pipeline_task_num=${parallelism}"
+        for (def numBuckets : [0, 1, 4, 16, 64, 256]) {
+            sql "set bucketed_agg_num_buckets=${numBuckets}"
+            String suffix = "${parallelism}_${numBuckets}"
+            queryCases.each { kind, query ->
+                explain {
+                    sql query
+                    contains "VBUCKETED AGGREGATE"
+                    contains "buckets: ${numBuckets == 0 ? 'auto' : 
numBuckets}"
+                }
+                assertEquals(referenceResults[kind], order_sql(query))
+                "order_qt_${kind}_${suffix}"(query)
+            }
+
+            // LimitAggToTopNAgg also adds TopN to an unordered LIMIT. Disable 
it only
+            // for this query so the bucketed operator exercises early 
termination.
+            // Any three groups are valid here; verify their invariants 
without a golden key order.
+            String limitQuery = """
+                SELECT /*+ SET_VAR(push_topn_to_agg=false) */ id, COUNT(*)
+                FROM bucketed_agg_num_buckets_test GROUP BY id LIMIT 3
+            """
+            explain {
+                sql limitQuery
+                contains "VBUCKETED AGGREGATE"
+                contains "buckets: ${numBuckets == 0 ? 'auto' : numBuckets}"
+            }
+            def limitRows = sql limitQuery
+            assertEquals(3, limitRows.size())
+            assertEquals(3, limitRows.collect { it[0] }.toSet().size())
+            limitRows.each { row ->
+                long key = (row[0] as Number).longValue()
+                assertTrue(key >= 0 && key < 32768)
+                assertEquals(1L, (row[1] as Number).longValue())
+                assertEquals(limitReference[row[0]], row[1])
+            }
+        }
+    }
+    for (def invalid : [-1, 3, 255, 257, 512]) {
+        test {
+            sql "set bucketed_agg_num_buckets=${invalid}"
+            exception "bucketed_agg_num_buckets must be 0 or a power of two 
between 1 and 256"
+        }
+    }
+}


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

Reply via email to