This is an automated email from the ASF dual-hosted git repository.
924060929 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 477b5883ebe [fix](local shuffle) Prevent row loss at
parallel-to-serial pipeline boundaries (#67177)
477b5883ebe is described below
commit 477b5883ebeca2bb6df027dffddc5100c3076791
Author: 924060929 <[email protected]>
AuthorDate: Mon Sep 14 18:31:30 2026 +0800
[fix](local shuffle) Prevent row loss at parallel-to-serial pipeline
boundaries (#67177)
### What problem does this PR solve?
Issue Number: None
Related PR: #65835
Problem Summary:
FE-planned local shuffle could implicitly collapse a parallel child
subtree into a serial consumer without an explicit gather. Remote
exchange receivers outside the first task could then be left unread,
producing incomplete scalar aggregate results.
The old BE-native planner also gave `PASS_TO_ONE` two meanings: it
created a gather for a shared broadcast hash table but silently changed
it to `BROADCAST` for private hash tables. That hidden reinterpretation
made the FE planner hard to reason about.
This PR makes the final distribution explicit:
- `PlanNode.enforceRequire` inserts `PASS_TO_ONE` at every non-serial
child to serial parent boundary.
- Shared broadcast hash-table builds choose `PASS_TO_ONE`; private
builds choose `BROADCAST`, in both FE planning and BE-native planning.
- BE exchanger factories honor the requested local partition type
literally.
- An FE-planned serial gather keeps all upstream senders but creates
exactly one downstream source/task.
No execution-version gate is added. FE local-shuffle planning has not
shipped on release branches, so supported rolling upgrades continue to
use the old FE with BE-native local-shuffle planning until FE is
upgraded.
### Release note
Fix incorrect results from FE-planned local shuffle at
parallel-to-serial pipeline boundaries.
---
be/src/exec/operator/hashjoin_build_sink.h | 10 +-
be/src/exec/pipeline/pipeline_fragment_context.cpp | 88 ++++++++--------
be/test/exec/operator/hashjoin_build_sink_test.cpp | 26 ++++-
be/test/exec/pipeline/local_exchanger_test.cpp | 69 ++++++++++--
.../org/apache/doris/planner/AnalyticEvalNode.java | 16 +--
.../org/apache/doris/planner/HashJoinNode.java | 6 +-
.../java/org/apache/doris/planner/PlanNode.java | 28 ++++-
.../planner/LocalShuffleNodeCoverageTest.java | 117 +++++++++++++++++++--
.../apache/doris/qe/LocalExchangePlannerTest.java | 30 ++++++
gensrc/thrift/Partitions.thrift | 6 +-
.../test_serial_aggregation_over_parallel_join.out | 18 ++++
...st_serial_aggregation_over_parallel_join.groovy | 97 +++++++++++++++++
12 files changed, 437 insertions(+), 74 deletions(-)
diff --git a/be/src/exec/operator/hashjoin_build_sink.h
b/be/src/exec/operator/hashjoin_build_sink.h
index 67b69f2fad4..e3eed65e01f 100644
--- a/be/src/exec/operator/hashjoin_build_sink.h
+++ b/be/src/exec/operator/hashjoin_build_sink.h
@@ -132,12 +132,16 @@ public:
._should_build_hash_table;
}
- DataDistribution required_data_distribution(RuntimeState* /*state*/) const
override {
+ DataDistribution required_data_distribution(RuntimeState* state) const
override {
if (_join_op == TJoinOp::NULL_AWARE_LEFT_ANTI_JOIN) {
return {TLocalPartitionType::NOOP};
} else if (_is_broadcast_join) {
- return _child->is_serial_operator() ?
DataDistribution(TLocalPartitionType::PASS_TO_ONE)
- :
DataDistribution(TLocalPartitionType::NOOP);
+ if (!_child->is_serial_operator()) {
+ return {TLocalPartitionType::NOOP};
+ }
+ return state->enable_share_hash_table_for_broadcast_join()
+ ? DataDistribution(TLocalPartitionType::PASS_TO_ONE)
+ : DataDistribution(TLocalPartitionType::BROADCAST);
}
return _join_distribution == TJoinDistributionType::BUCKET_SHUFFLE ||
_join_distribution ==
TJoinDistributionType::COLOCATE
diff --git a/be/src/exec/pipeline/pipeline_fragment_context.cpp
b/be/src/exec/pipeline/pipeline_fragment_context.cpp
index f62bd073081..5fa5c4c2932 100644
--- a/be/src/exec/pipeline/pipeline_fragment_context.cpp
+++ b/be/src/exec/pipeline/pipeline_fragment_context.cpp
@@ -719,6 +719,7 @@ Status
PipelineFragmentContext::_build_pipelines(ObjectPool* pool, const Descrip
Status PipelineFragmentContext::_create_deferred_local_exchangers() {
for (auto& info : _deferred_exchangers) {
+ const int source_count =
cast_set<int>(info.shared_state->source_deps.size());
// DANGER ZONE — do not "fix" this line without reading the history.
//
// sender_count seeds Exchanger::_running_sink_operators, which the
source side
@@ -751,34 +752,29 @@ Status
PipelineFragmentContext::_create_deferred_local_exchangers() {
switch (info.partition_type) {
case TLocalPartitionType::LOCAL_EXECUTION_HASH_SHUFFLE:
case TLocalPartitionType::GLOBAL_EXECUTION_HASH_SHUFFLE:
- info.shared_state->exchanger = ShuffleExchanger::create_unique(
- sender_count, _num_instances, info.num_partitions,
info.free_blocks_limit,
- info.partition_type);
+ info.shared_state->exchanger =
+ ShuffleExchanger::create_unique(sender_count,
source_count, info.num_partitions,
+ info.free_blocks_limit,
info.partition_type);
break;
case TLocalPartitionType::BUCKET_HASH_SHUFFLE:
info.shared_state->exchanger =
BucketShuffleExchanger::create_unique(
- sender_count, _num_instances, info.num_partitions,
info.free_blocks_limit);
+ sender_count, source_count, info.num_partitions,
info.free_blocks_limit);
break;
case TLocalPartitionType::PASSTHROUGH:
info.shared_state->exchanger = PassthroughExchanger::create_unique(
- sender_count, _num_instances, info.free_blocks_limit);
+ sender_count, source_count, info.free_blocks_limit);
break;
case TLocalPartitionType::BROADCAST:
info.shared_state->exchanger = BroadcastExchanger::create_unique(
- sender_count, _num_instances, info.free_blocks_limit);
+ sender_count, source_count, info.free_blocks_limit);
break;
case TLocalPartitionType::PASS_TO_ONE:
- if (_runtime_state->enable_share_hash_table_for_broadcast_join()) {
- info.shared_state->exchanger =
PassToOneExchanger::create_unique(
- sender_count, _num_instances, info.free_blocks_limit);
- } else {
- info.shared_state->exchanger =
BroadcastExchanger::create_unique(
- sender_count, _num_instances, info.free_blocks_limit);
- }
+ info.shared_state->exchanger = PassToOneExchanger::create_unique(
+ sender_count, source_count, info.free_blocks_limit);
break;
case TLocalPartitionType::ADAPTIVE_PASSTHROUGH:
info.shared_state->exchanger =
AdaptivePassthroughExchanger::create_unique(
- sender_count, _num_instances, info.free_blocks_limit);
+ sender_count, source_count, info.free_blocks_limit);
break;
case TLocalPartitionType::NOOP:
case TLocalPartitionType::LOCAL_MERGE_SORT:
@@ -855,11 +851,17 @@ void
PipelineFragmentContext::_propagate_local_exchange_num_tasks() {
if (pit != id_to_pipe.end()) {
auto& pipe = pit->second;
const auto& ops = pipe->operators();
- const bool le_source =
- !ops.empty() &&
dynamic_cast<LocalExchangeSourceOperatorX*>(ops.front().get());
+ auto* le_source =
+ !ops.empty() ?
dynamic_cast<LocalExchangeSourceOperatorX*>(ops.front().get())
+ : nullptr;
const bool serial_source = !ops.empty() &&
ops.front()->is_serial_operator();
if (le_source) {
- pipe->set_num_tasks(_num_instances);
+ // PASS_TO_ONE is the explicit N-to-one boundary. Its upstream
pipeline
+ // keeps all active tasks, while only fragment instance 0
creates the
+ // downstream serial pipeline task.
+ if (le_source->exchange_type() !=
TLocalPartitionType::PASS_TO_ONE) {
+ pipe->set_num_tasks(_num_instances);
+ }
} else if (!serial_source) {
int target = pipe->num_tasks();
const auto up_it = _dag.find(id);
@@ -1072,22 +1074,12 @@ Status
PipelineFragmentContext::_add_local_exchange_impl(
: 0);
break;
case TLocalPartitionType::PASS_TO_ONE:
- if (_runtime_state->enable_share_hash_table_for_broadcast_join()) {
- // If shared hash table is enabled for BJ, hash table will be
built by only one task
- shared_state->exchanger = PassToOneExchanger::create_unique(
- cur_pipe->num_tasks(), _num_instances,
-
_runtime_state->query_options().__isset.local_exchange_free_blocks_limit
- ? cast_set<int>(_runtime_state->query_options()
-
.local_exchange_free_blocks_limit)
- : 0);
- } else {
- shared_state->exchanger = BroadcastExchanger::create_unique(
- cur_pipe->num_tasks(), _num_instances,
-
_runtime_state->query_options().__isset.local_exchange_free_blocks_limit
- ? cast_set<int>(_runtime_state->query_options()
-
.local_exchange_free_blocks_limit)
- : 0);
- }
+ shared_state->exchanger = PassToOneExchanger::create_unique(
+ cur_pipe->num_tasks(), _num_instances,
+
_runtime_state->query_options().__isset.local_exchange_free_blocks_limit
+ ? cast_set<int>(
+
_runtime_state->query_options().local_exchange_free_blocks_limit)
+ : 0);
break;
case TLocalPartitionType::ADAPTIVE_PASSTHROUGH:
shared_state->exchanger = AdaptivePassthroughExchanger::create_unique(
@@ -2046,9 +2038,12 @@ Status
PipelineFragmentContext::_create_operator(ObjectPool* pool, const TPlanNo
}
case TPlanNodeType::LOCAL_EXCHANGE_NODE: {
op = std::make_shared<LocalExchangeSourceOperatorX>(pool, tnode,
next_operator_id(), descs);
- // The downstream pipeline (containing LocalExchangeSource) must have
- // _num_instances tasks — matching BE-native
_inherit_pipeline_properties
- // which sets pipe_with_source.set_num_tasks(_num_instances).
+ const auto partition_type = tnode.local_exchange_node.partition_type;
+ const bool pass_to_one = partition_type ==
TLocalPartitionType::PASS_TO_ONE;
+ // Except at an explicit PASS_TO_ONE boundary, the downstream pipeline
+ // (containing LocalExchangeSource) must have _num_instances tasks.
This
+ // matches BE-native _inherit_pipeline_properties, which sets
+ // pipe_with_source.set_num_tasks(_num_instances).
// Without this, when the parent pipeline was reduced by a serial
operator
// (e.g., serial Exchange with use_serial_exchange=true, or
UNPARTITIONED
// Exchange), the downstream inherits the reduced num_tasks via
@@ -2057,14 +2052,23 @@ Status
PipelineFragmentContext::_create_operator(ObjectPool* pool, const TPlanNo
// sink round-robins to all channels and crashes on uninitialized ones.
RETURN_IF_ERROR(cur_pipe->add_operator(op, _parallel_instances));
// Restore downstream pipeline's num_tasks (mirroring
_inherit_pipeline_properties:
- // downstream keeps _num_instances, upstream gets the serial/reduced
count)
- cur_pipe->set_num_tasks(_num_instances);
+ // downstream keeps _num_instances, upstream gets the serial/reduced
count).
+ // PASS_TO_ONE is the explicit parallel-to-serial boundary: its
downstream
+ // pipeline must keep the serial parent's single active task, while
the upstream
+ // pipeline is expanded below so every fragment instance keeps an
active receiver.
+ if (!pass_to_one) {
+ cur_pipe->set_num_tasks(_num_instances);
+ }
+ const int downstream_num_tasks = cur_pipe->num_tasks();
const auto downstream_pipeline_id = cur_pipe->id();
if (!_dag.contains(downstream_pipeline_id)) {
_dag.insert({downstream_pipeline_id, {}});
}
cur_pipe = add_pipeline(cur_pipe);
+ if (pass_to_one) {
+ cur_pipe->set_num_tasks(_num_instances);
+ }
// If this local exchange was inserted because of a serial scan
(is_serial_operator),
// the upstream pipeline (cur_pipe) should have num_tasks=1 (only 1
scan task).
// We set this now so the exchanger is created with the correct sender
count.
@@ -2076,7 +2080,6 @@ Status
PipelineFragmentContext::_create_operator(ObjectPool* pool, const TPlanNo
_dag[downstream_pipeline_id].push_back(cur_pipe->id());
int num_partitions = 0;
std::map<int, int> shuffle_id_to_instance_idx;
- auto partition_type = tnode.local_exchange_node.partition_type;
switch (partition_type) {
case TLocalPartitionType::BUCKET_HASH_SHUFFLE:
num_partitions = _params.num_buckets;
@@ -2116,9 +2119,10 @@ Status
PipelineFragmentContext::_create_operator(ObjectPool* pool, const TPlanNo
? cast_set<int>(
_runtime_state->query_options().local_exchange_free_blocks_limit)
: 0;
- auto shared_state =
LocalExchangeSharedState::create_shared(_num_instances);
- shared_state->create_source_dependencies(_num_instances,
local_exchange_id,
- local_exchange_id,
"LOCAL_EXCHANGE_OPERATOR");
+ const int source_count = downstream_num_tasks;
+ auto shared_state =
LocalExchangeSharedState::create_shared(source_count);
+ shared_state->create_source_dependencies(source_count,
local_exchange_id, local_exchange_id,
+ "LOCAL_EXCHANGE_OPERATOR");
shared_state->create_sink_dependency(sink_id, local_exchange_id,
"LOCAL_EXCHANGE_SINK");
_op_id_to_shared_state.insert({local_exchange_id, {shared_state,
shared_state->sink_deps}});
// Defer exchanger creation: sender count depends on final upstream
num_tasks
diff --git a/be/test/exec/operator/hashjoin_build_sink_test.cpp
b/be/test/exec/operator/hashjoin_build_sink_test.cpp
index 0cabf71258f..35cbb15fe92 100644
--- a/be/test/exec/operator/hashjoin_build_sink_test.cpp
+++ b/be/test/exec/operator/hashjoin_build_sink_test.cpp
@@ -318,6 +318,30 @@ TEST_F(HashJoinBuildSinkTest, Sink) {
run_test_block(test_block);
}
+TEST_F(HashJoinBuildSinkTest, BroadcastJoinRequiredDataDistribution) {
+ auto tnode = _helper.create_test_plan_node(TJoinOp::INNER_JOIN,
{TPrimitiveType::INT}, {false},
+ {false});
+ tnode.hash_join_node.__set_is_broadcast_join(true);
+ auto [probe_operator, sink_operator] = _helper.create_operators(tnode);
+ ASSERT_TRUE(probe_operator);
+ ASSERT_TRUE(sink_operator);
+
+
EXPECT_EQ(sink_operator->required_data_distribution(_helper.runtime_state.get())
+ .distribution_type,
+ TLocalPartitionType::NOOP);
+
+ sink_operator->child()->set_serial_operator();
+ _helper.runtime_state->_enable_share_hash_table_for_broadcast_join = true;
+
EXPECT_EQ(sink_operator->required_data_distribution(_helper.runtime_state.get())
+ .distribution_type,
+ TLocalPartitionType::PASS_TO_ONE);
+
+ _helper.runtime_state->_enable_share_hash_table_for_broadcast_join = false;
+
EXPECT_EQ(sink_operator->required_data_distribution(_helper.runtime_state.get())
+ .distribution_type,
+ TLocalPartitionType::BROADCAST);
+}
+
TEST_F(HashJoinBuildSinkTest, Terminate) {
auto test_block = [&](TJoinOp::type op_type, const
std::vector<TPrimitiveType::type>& key_types,
const std::vector<bool>& left_nullables,
@@ -695,4 +719,4 @@ TEST_F(SharedHashTableSignalTest,
MultipleNonBuildersAllReturnEOFWhenTerminated)
}
}
-} // namespace doris
\ No newline at end of file
+} // namespace doris
diff --git a/be/test/exec/pipeline/local_exchanger_test.cpp
b/be/test/exec/pipeline/local_exchanger_test.cpp
index 0967c6758bd..09c3dfa26c0 100644
--- a/be/test/exec/pipeline/local_exchanger_test.cpp
+++ b/be/test/exec/pipeline/local_exchanger_test.cpp
@@ -29,12 +29,14 @@
#include "exec/exchange/local_exchange_sink_operator.h"
#include "exec/exchange/local_exchange_source_operator.h"
#include "exec/pipeline/dependency.h"
+#include "exec/pipeline/pipeline_fragment_context.h"
#include "exec/pipeline/thrift_builder.h"
#include "exprs/vslot_ref.h"
+#include "runtime/descriptor_helper.h"
namespace doris {
-class LocalExchangerTest : public testing::Test {
+class LocalExchangerTest : public testing::TestWithParam<int> {
public:
LocalExchangerTest() = default;
~LocalExchangerTest() override = default;
@@ -532,9 +534,60 @@ TEST_F(LocalExchangerTest, PassthroughExchanger) {
}
}
-TEST_F(LocalExchangerTest, PassToOneExchanger) {
+TEST_F(LocalExchangerTest, FePlannedPassToOneUsesOneDownstreamSource) {
+ constexpr int num_instances = 4;
+ _query_options.__set_enable_share_hash_table_for_broadcast_join(false);
+ TPipelineFragmentParams params;
+ auto context = std::make_shared<PipelineFragmentContext>(
+ _query_id, params, _query_ctx, ExecEnv::GetInstance(),
[](RuntimeState*, Status*) {});
+ context->_num_instances = num_instances;
+ context->_total_instances = num_instances;
+ context->_runtime_state = RuntimeState::create_unique(_query_id,
_fragment_id, _query_options,
+
_query_ctx->query_globals,
+
ExecEnv::GetInstance(), _query_ctx.get());
+
+ auto downstream_pipe = context->add_pipeline();
+ downstream_pipe->set_num_tasks(1);
+ auto upstream_pipe = downstream_pipe;
+
+ TLocalExchangeNode local_exchange_node;
+ local_exchange_node.__set_partition_type(TLocalPartitionType::PASS_TO_ONE);
+ TPlanNode tnode;
+ tnode.__set_node_type(TPlanNodeType::LOCAL_EXCHANGE_NODE);
+ tnode.__set_node_id(0);
+ tnode.__set_num_children(1);
+ tnode.__set_local_exchange_node(local_exchange_node);
+ tnode.__set_row_tuples({0});
+
+ ObjectPool pool;
+ TDescriptorTableBuilder desc_builder;
+ TTupleDescriptorBuilder().build(&desc_builder);
+ DescriptorTbl* descs = nullptr;
+ ASSERT_TRUE(DescriptorTbl::create(&pool, desc_builder.desc_tbl(),
&descs).ok());
+ OperatorPtr op;
+ OperatorPtr cache_op;
+ ASSERT_TRUE(context->_create_operator(&pool, tnode, *descs, op,
upstream_pipe,
+ /*parent_idx=*/-1, /*child_idx=*/0,
+
/*followed_by_shuffled_operator=*/false,
+
/*require_bucket_distribution=*/false, cache_op)
+ .ok());
+ ASSERT_EQ(context->_deferred_exchangers.size(), 1);
+ EXPECT_EQ(downstream_pipe->num_tasks(), 1);
+ EXPECT_EQ(upstream_pipe->num_tasks(), num_instances);
+
+ auto shared_state = context->_deferred_exchangers.front().shared_state;
+ EXPECT_EQ(shared_state->source_deps.size(), 1);
+ EXPECT_EQ(shared_state->mem_counters.size(), 1);
+ ASSERT_TRUE(context->_create_deferred_local_exchangers().ok());
+ ASSERT_NE(shared_state->exchanger, nullptr);
+ EXPECT_EQ(shared_state->exchanger->get_type(),
TLocalPartitionType::PASS_TO_ONE);
+ EXPECT_EQ(shared_state->exchanger->_num_senders, num_instances);
+ EXPECT_EQ(shared_state->exchanger->_num_sources, 1);
+}
+
+TEST_P(LocalExchangerTest, PassToOneExchanger) {
int num_sink = 4;
- int num_sources = 4;
+ int num_sources = GetParam();
int free_block_limit = 0;
const auto expect_block_bytes = 128;
@@ -555,6 +608,8 @@ TEST_F(LocalExchangerTest, PassToOneExchanger) {
shared_state->create_source_dependencies(num_sources, 0, 0, "TEST");
auto* exchanger = (PassToOneExchanger*)shared_state->exchanger.get();
+ EXPECT_EQ(exchanger->_num_senders, num_sink);
+ EXPECT_EQ(exchanger->_num_sources, num_sources);
for (size_t i = 0; i < num_sink; i++) {
auto* compute_hash_value_timer =
ADD_TIMER(profile, "ComputeHashValueTime" + std::to_string(i));
@@ -585,10 +640,9 @@ TEST_F(LocalExchangerTest, PassToOneExchanger) {
"MemoryUsage" + std::to_string(i), TUnit::BYTES, "", 1);
shared_state->mem_counters[i] = _local_states[i]->_memory_used_counter;
}
-
{
- // Enqueue `num_blocks` blocks with 10 rows for each data queue.
- for (size_t i = 0; i < num_sources; i++) {
+ // Enqueue `num_blocks` blocks with 10 rows from every sender.
+ for (size_t i = 0; i < num_sink; i++) {
for (size_t j = 0; j < num_blocks; j++) {
Block in_block;
DataTypePtr int_type = std::make_shared<DataTypeInt32>();
@@ -744,6 +798,8 @@ TEST_F(LocalExchangerTest, PassToOneExchanger) {
}
}
+INSTANTIATE_TEST_SUITE_P(SourceCardinality, LocalExchangerTest,
testing::Values(4, 1));
+
TEST_F(LocalExchangerTest, BroadcastExchanger) {
int num_sink = 4;
int num_sources = 4;
@@ -1397,4 +1453,5 @@ TEST_F(LocalExchangerTest,
ShuffleExchangerRestoreOutputBlockOnAddRowsError) {
EXPECT_EQ(output_block.rows(), 1);
EXPECT_NO_THROW(output_block.check_number_of_rows());
}
+
} // namespace doris
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/planner/AnalyticEvalNode.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/AnalyticEvalNode.java
index 1698b25495d..f530875ccdd 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/AnalyticEvalNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/AnalyticEvalNode.java
@@ -209,17 +209,19 @@ public class AnalyticEvalNode extends PlanNode {
LocalExchangeType outputType = null;
if (partitionExprs.isEmpty()) {
// Serial AnalyticEval (OVER() with no PARTITION BY):
- // Must NOT have any LocalExchange between AnalyticEval and its
child.
- // On BE, AnalyticSink and AnalyticSource share state
(source_deps/sink_deps).
- // A LocalExchange below would restore the AnalyticSink pipeline
to _num_instances
- // tasks while the serial AnalyticSource pipeline stays at 1 task.
+ // Do not keep a redundant LocalExchange between AnalyticEval and
an already
+ // serial child. On BE, AnalyticSink and AnalyticSource share state
+ // (source_deps/sink_deps), so restoring only the sink pipeline to
+ // _num_instances tasks would mismatch the serial source pipeline.
//
- // Use enforceRequire with noRequire to traverse children, then
strip any
- // LocalExchange the child inserted (e.g., Exchange wrapping
itself with PASSTHROUGH).
+ // PASS_TO_ONE is different: enforceRequire inserts it when the
child subtree is
+ // parallel. It is the explicit N-to-one boundary that keeps every
upstream task
+ // active while leaving the analytic sink/source pair at one task,
so retain it.
Pair<PlanNode, LocalExchangeType> enforceResult
= enforceRequire(translatorContext, children.get(0), 0,
LocalExchangeTypeRequire.noRequire());
PlanNode newChild = enforceResult.first;
- if (newChild instanceof LocalExchangeNode) {
+ if (newChild instanceof LocalExchangeNode
+ && ((LocalExchangeNode) newChild).getExchangeType() !=
LocalExchangeType.PASS_TO_ONE) {
newChild = newChild.getChild(0);
}
children = Lists.newArrayList(newChild);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/planner/HashJoinNode.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/HashJoinNode.java
index e9d8ac63041..1fc7880e9b2 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/HashJoinNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/HashJoinNode.java
@@ -338,7 +338,11 @@ public class HashJoinNode extends JoinNodeBase {
? LocalExchangeTypeRequire.requirePassthrough()
: LocalExchangeTypeRequire.noRequire();
buildSideRequire = buildChildSerial
- ? LocalExchangeTypeRequire.requirePassToOne()
+ ? (translatorContext.getConnectContext() == null
+ ||
translatorContext.getConnectContext().getSessionVariable()
+ .enableShareHashTableForBroadcastJoin
+ ? LocalExchangeTypeRequire.requirePassToOne()
+ : LocalExchangeTypeRequire.requireBroadcast())
: LocalExchangeTypeRequire.noRequire();
// For serial or force-passthrough probe: output is PASSTHROUGH.
// For a non-serial probe without the flag: propagate the probe's
distribution.
diff --git a/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java
index b592c15f627..580dc9a4eaa 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java
@@ -1084,11 +1084,19 @@ public abstract class PlanNode extends
TreeNode<PlanNode> {
// serial-source mode, BE treats this operator as non-serial
regardless of isSerialNode.
// Using isSerialNode here would set the child's serial-ancestor flag
wider than BE's
// view and over-skip required LocalExchanges downstream.
- boolean childHasSerialAncestor = inheritedSerial
- || currentNodeSerialOnBe.get();
+ boolean selfSerial = currentNodeSerialOnBe.get();
+ boolean passToOneAtSerialBoundary = selfSerial
+ &&
!child.isSerialOperatorOnBe(translatorContext.getConnectContext());
+ // PASS_TO_ONE becomes the pipeline boundary between this serial
consumer and the
+ // parallel child subtree. Do not let either serial marker cross that
boundary: the
+ // child must still plan the local exchanges required by its own
parallel pipelines.
+ boolean childHasSerialAncestor = passToOneAtSerialBoundary
+ ? false : inheritedSerial || selfSerial;
+ boolean childHasSerialParentPipeline = passToOneAtSerialBoundary
+ ? false : startsNewPipeline
+ ? currentPipelineSerial :
translatorContext.hasSerialParentPipeline(this);
translatorContext.setHasSerialAncestorInPipeline(child,
childHasSerialAncestor);
- translatorContext.setHasSerialParentPipeline(child, startsNewPipeline
- ? currentPipelineSerial :
translatorContext.hasSerialParentPipeline(this));
+ translatorContext.setHasSerialParentPipeline(child,
childHasSerialParentPipeline);
// 1b. Propagate shuffle-for-correctness-ancestor flag to child.
// Mirrors BE's _followed_by_shuffled_operator: a downstream operator
needs hash
@@ -1109,6 +1117,18 @@ public abstract class PlanNode extends
TreeNode<PlanNode> {
Pair<PlanNode, LocalExchangeType> childOutput =
child.enforceAndDeriveLocalExchange(translatorContext, this,
require);
+ // A serial consumer must not implicitly reduce a non-serial subtree
to one pipeline
+ // task. Besides losing parallelism, that can make a remote Exchange
expose fewer
+ // receiver tasks than FE addresses. Keep the subtree parallel and
make the N-to-one
+ // transition explicit. PASS_TO_ONE keeps every upstream receiver task
alive and
+ // funnels their output into the serial downstream pipeline's only
task.
+ if (passToOneAtSerialBoundary && childOutput.second !=
LocalExchangeType.PASS_TO_ONE) {
+ childOutput = Pair.of(
+ createLocalExchange(translatorContext, childOutput.first,
+ LocalExchangeType.PASS_TO_ONE, null),
+ LocalExchangeType.PASS_TO_ONE);
+ }
+
// Steps 2.5 and 3 both react to a serial child but address different
concerns:
// - Step 2.5 rewrites the OUTPUT-side view (what we tell
satisfy/parent about
// the child's actual distribution). A serial pipeline runs with
1 task so
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/planner/LocalShuffleNodeCoverageTest.java
b/fe/fe-core/src/test/java/org/apache/doris/planner/LocalShuffleNodeCoverageTest.java
index 59c26166c3f..e8bcbb195aa 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/planner/LocalShuffleNodeCoverageTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/planner/LocalShuffleNodeCoverageTest.java
@@ -547,6 +547,26 @@ public class LocalShuffleNodeCoverageTest {
+ "even if isSerialNode()=true — BE treats the node as
non-serial.");
}
+ @Test
+ public void testPassToOneBoundaryKeepsParallelSubtreeLocalExchange() {
+ PlanTranslatorContext ctx = new PlanTranslatorContext();
+ TrackingPlanNode leaf = new TrackingPlanNode(nextPlanNodeId(),
LocalExchangeType.NOOP);
+ HashRequiringPlanNode parallelSubtree = new
HashRequiringPlanNode(nextPlanNodeId(), leaf);
+ SerialPipelineBoundaryNode serialParent = new
SerialPipelineBoundaryNode(
+ nextPlanNodeId(), parallelSubtree);
+ serialParent.fragment = Mockito.mock(PlanFragment.class);
+
Mockito.when(serialParent.fragment.useSerialSource(Mockito.any())).thenReturn(true);
+
+ Pair<PlanNode, LocalExchangeType> output =
serialParent.enforceAndDeriveLocalExchange(
+ ctx, null, LocalExchangeTypeRequire.noRequire());
+
+ Assertions.assertEquals(LocalExchangeType.PASS_TO_ONE, output.second);
+ assertChildLocalExchangeType(serialParent, 0,
LocalExchangeType.PASS_TO_ONE);
+ Assertions.assertSame(parallelSubtree,
serialParent.getChild(0).getChild(0));
+ assertChildLocalExchangeType(parallelSubtree, 0,
+ LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+ }
+
@Test
public void testNestedLoopJoinNodeBranches() {
PlanTranslatorContext ctx = new PlanTranslatorContext();
@@ -753,8 +773,10 @@ public class LocalShuffleNodeCoverageTest {
// Output is still PASSTHROUGH (hardcoded for useSerialSource +
ScanNode child).
SerialTrackingScanNode serialScan = new
SerialTrackingScanNode(nextPlanNodeId(), LocalExchangeType.NOOP);
SortNode scanSort = new SortNode(nextPlanNodeId(), serialScan,
sortInfo, false);
- scanSort.fragment = Mockito.mock(PlanFragment.class);
-
Mockito.when(scanSort.fragment.useSerialSource(Mockito.any())).thenReturn(true);
+ PlanFragment serialSortFragment = Mockito.mock(PlanFragment.class);
+
Mockito.when(serialSortFragment.useSerialSource(Mockito.any())).thenReturn(true);
+ scanSort.setFragment(serialSortFragment);
+ serialScan.setFragment(serialSortFragment);
Pair<PlanNode, LocalExchangeType> scanOutput =
scanSort.enforceAndDeriveLocalExchange(
ctx, null, LocalExchangeTypeRequire.noRequire());
// Non-merge, non-analytic SortNode: isSerialNode()=true,
requireChild=noRequire,
@@ -809,6 +831,24 @@ public class LocalShuffleNodeCoverageTest {
Assertions.assertEquals(LocalExchangeType.NOOP,
noPartitionOutput.second);
Assertions.assertSame(noPartitionChild, noPartition.getChild(0));
+ // A serial analytic consumer over a parallel subtree needs the generic
+ // parallel-to-serial boundary inserted by PlanNode.enforceRequire.
The analytic
+ // special case may remove a redundant exchange directly above a
serial Exchange,
+ // but must retain this PASS_TO_ONE gather.
+ TrackingPlanNode parallelChild = new
TrackingPlanNode(nextPlanNodeId(), LocalExchangeType.NOOP);
+ AnalyticEvalNode serialOverParallel = new
AnalyticEvalNode(nextPlanNodeId(), parallelChild,
+ Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(),
+ null, new TupleDescriptor(new
TupleId(NEXT_ID.getAndIncrement())));
+ PlanFragment serialAnalyticFragment = Mockito.mock(PlanFragment.class);
+
Mockito.when(serialAnalyticFragment.useSerialSource(Mockito.any())).thenReturn(true);
+ serialOverParallel.setFragment(serialAnalyticFragment);
+ parallelChild.setFragment(serialAnalyticFragment);
+ Pair<PlanNode, LocalExchangeType> serialOverParallelOutput
+ = serialOverParallel.enforceAndDeriveLocalExchange(
+ ctx, null, LocalExchangeTypeRequire.noRequire());
+ Assertions.assertEquals(LocalExchangeType.NOOP,
serialOverParallelOutput.second);
+ assertChildLocalExchangeType(serialOverParallel, 0,
LocalExchangeType.PASS_TO_ONE);
+
// Analytic with partition but no orderBy, non-colocated →
noRequire/NOOP.
// (Non-colocated analytic relies on parent SortNode to handle
distribution.)
TrackingScanNode hashChild = new TrackingScanNode(nextPlanNodeId(),
LocalExchangeType.NOOP);
@@ -826,12 +866,14 @@ public class LocalShuffleNodeCoverageTest {
Collections.emptyList(),
Collections.singletonList(Mockito.mock(Expr.class)),
Collections.singletonList(new
OrderByElement(Mockito.mock(Expr.class), true, true)),
null, new TupleDescriptor(new
TupleId(NEXT_ID.getAndIncrement())));
- orderedAnalytic.fragment = Mockito.mock(PlanFragment.class);
-
Mockito.when(orderedAnalytic.fragment.useSerialSource(Mockito.any())).thenReturn(true);
+ PlanFragment orderedAnalyticFragment =
Mockito.mock(PlanFragment.class);
+
Mockito.when(orderedAnalyticFragment.useSerialSource(Mockito.any())).thenReturn(true);
+ orderedAnalytic.setFragment(orderedAnalyticFragment);
+ serialScan.setFragment(orderedAnalyticFragment);
Pair<PlanNode, LocalExchangeType> orderedOutput =
orderedAnalytic.enforceAndDeriveLocalExchange(
ctx, null, LocalExchangeTypeRequire.noRequire());
- // Serial AnalyticEval returns NOOP — lets framework serial check
handle fan-out
- Assertions.assertEquals(LocalExchangeType.NOOP, orderedOutput.second);
+ Assertions.assertEquals(LocalExchangeType.PASSTHROUGH,
orderedOutput.second);
+ assertChildLocalExchangeType(orderedAnalytic, 0,
LocalExchangeType.PASSTHROUGH);
}
@Test
@@ -1210,6 +1252,69 @@ public class LocalShuffleNodeCoverageTest {
}
}
+ private static class SerialPipelineBoundaryNode extends PlanNode {
+ SerialPipelineBoundaryNode(PlanNodeId id, PlanNode child) {
+ super(id, Lists.newArrayList(new TupleId(id.asInt() + 30000)),
+ "SERIAL_PIPELINE_BOUNDARY");
+ children.add(child);
+ }
+
+ @Override
+ public boolean isSerialNode() {
+ return true;
+ }
+
+ @Override
+ protected boolean shouldResetSerialFlagForChild(int childIndex) {
+ return true;
+ }
+
+ @Override
+ public Pair<PlanNode, LocalExchangeType> enforceAndDeriveLocalExchange(
+ PlanTranslatorContext translatorContext, PlanNode parent,
+ LocalExchangeTypeRequire parentRequire) {
+ Pair<PlanNode, LocalExchangeType> result =
enforceRequire(translatorContext,
+ children.get(0), 0, LocalExchangeTypeRequire.noRequire());
+ children = Lists.newArrayList(result.first);
+ return Pair.of(this, result.second);
+ }
+
+ @Override
+ protected void toThrift(TPlanNode msg) {
+ }
+
+ @Override
+ public String getNodeExplainString(String prefix, TExplainLevel
detailLevel) {
+ return "";
+ }
+ }
+
+ private static class HashRequiringPlanNode extends PlanNode {
+ HashRequiringPlanNode(PlanNodeId id, PlanNode child) {
+ super(id, Lists.newArrayList(new TupleId(id.asInt() + 40000)),
"HASH_REQUIRING");
+ children.add(child);
+ }
+
+ @Override
+ public Pair<PlanNode, LocalExchangeType> enforceAndDeriveLocalExchange(
+ PlanTranslatorContext translatorContext, PlanNode parent,
+ LocalExchangeTypeRequire parentRequire) {
+ Pair<PlanNode, LocalExchangeType> result =
enforceRequire(translatorContext,
+ children.get(0), 0,
LocalExchangeTypeRequire.requireHash());
+ children = Lists.newArrayList(result.first);
+ return Pair.of(this, result.second);
+ }
+
+ @Override
+ protected void toThrift(TPlanNode msg) {
+ }
+
+ @Override
+ public String getNodeExplainString(String prefix, TExplainLevel
detailLevel) {
+ return "";
+ }
+ }
+
private static class TrackingPlanNode extends PlanNode {
private final LocalExchangeType providedType;
private LocalExchangeTypeRequire lastRequire;
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/qe/LocalExchangePlannerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/qe/LocalExchangePlannerTest.java
index d691028666f..7fead508744 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/qe/LocalExchangePlannerTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/qe/LocalExchangePlannerTest.java
@@ -75,6 +75,7 @@ public class LocalExchangePlannerTest extends
TestWithFeService implements PlanS
sv.setEnableLocalShufflePlanner(true);
sv.setEnableLocalShuffle(true);
sv.setEnableNereidsDistributePlanner(true);
+ sv.setEnableSqlCache(false);
sv.setIgnoreStorageDataDistribution(true);
sv.setPipelineTaskNum("4");
sv.setForceToLocalShuffle(false);
@@ -83,6 +84,7 @@ public class LocalExchangePlannerTest extends
TestWithFeService implements PlanS
// the default strategy) before applying this test's own tweaks.
sv.aggPhase = 0;
sv.enableBroadcastJoinForcePassthrough = false;
+ sv.enableShareHashTableForBroadcastJoin = true;
if (tweaks != null) {
tweaks.accept(sv);
}
@@ -169,6 +171,22 @@ public class LocalExchangePlannerTest extends
TestWithFeService implements PlanS
olapScan("t1")))))));
}
+ @Test
+ public void testSerialDistinctAggGathersParallelJoinOutput() throws
Exception {
+ setupLocalShuffleSession(sv -> sv.enableShareHashTableForBroadcastJoin
= false);
+ connectContext.getSessionVariable().setPipelineTaskNum("3");
+ assertPlanShape(
+ "select sum(distinct a.k1) from test.t1 a "
+ + "left join test.t2 b on a.k1 = b.k1",
+ anyTree(
+ agg(
+ localExchange(PASS_TO_ONE_LE,
+ hashJoin(
+ localExchange(PT, olapScan()),
+ localExchange(BROADCAST_LE,
+
anyTree(exchange())))))));
+ }
+
@Test
public void testCountDistinctNoGroupByRequiresHashBeforeAgg() throws
Exception {
// count(distinct k2) without group-by: the finalize merge agg emits
per-instance
@@ -286,6 +304,18 @@ public class LocalExchangePlannerTest extends
TestWithFeService implements PlanS
anyTree(exchange())))));
}
+ @Test
+ public void testPrivateBroadcastJoinBuildUsesBroadcastLocalExchange()
throws Exception {
+ setupLocalShuffleSession(sv -> sv.enableShareHashTableForBroadcastJoin
= false);
+ assertPlanShape("select * from test.t1 a join [broadcast] test.t2 b on
a.k1=b.k1",
+ anyTree(
+ hashJoin(
+ localExchange(PT,
+ olapScan()),
+ localExchange(BROADCAST_LE,
+ anyTree(exchange())))));
+ }
+
@Test
public void testNlJoinPoolingShapeDsl() throws Exception {
// doc rule "NL join / 池化": build BROADCAST, probe
ADAPTIVE_PASSTHROUGH.
diff --git a/gensrc/thrift/Partitions.thrift b/gensrc/thrift/Partitions.thrift
index da172fac735..b14e36a3f62 100644
--- a/gensrc/thrift/Partitions.thrift
+++ b/gensrc/thrift/Partitions.thrift
@@ -128,10 +128,8 @@ enum TLocalPartitionType {
// Scan(build side) -> LocalExchangeNode(BROADCAST) -> HashJoin(build)
BROADCAST = 6,
// PASS_TO_ONE: funnel all rows to a single local instance (channel 0);
every other instance gets EOS
- // immediately and produces nothing (PassToOneExchanger). used for a
broadcast join with a shared
- // hash table, where only instance 0 needs the build data and the others
share its hash table.
- // NOTE: BE only uses PassToOneExchanger when
`enable_share_hash_table_for_broadcast_join` is on;
- // when it is off the same PASS_TO_ONE type degrades to BROADCAST (each
instance keeps its own copy).
+ // immediately and produces nothing (PassToOneExchanger). Used at
parallel-to-serial boundaries and
+ // for a broadcast join with a shared hash table. A private broadcast hash
table uses BROADCAST.
PASS_TO_ONE = 7,
// LOCAL_MERGE_SORT: k-way merge of several already-sorted local inputs into
one globally sorted
// stream on a single instance (paired with LocalMergeSortSourceOperator,
for a SortNode with
diff --git
a/regression-test/data/nereids_p0/local_shuffle/test_serial_aggregation_over_parallel_join.out
b/regression-test/data/nereids_p0/local_shuffle/test_serial_aggregation_over_parallel_join.out
new file mode 100644
index 00000000000..0ef93e2a601
--- /dev/null
+++
b/regression-test/data/nereids_p0/local_shuffle/test_serial_aggregation_over_parallel_join.out
@@ -0,0 +1,18 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !count_distinct_left_join --
+10
+
+-- !sum_and_count_distinct_left_join --
+23 5
+
+-- !sum_distinct_broad_predicate --
+45
+
+-- !sum_distinct_reversed_join --
+45
+
+-- !sum_distinct_multi_outer_join --
+35
+
+-- !native_private_broadcast_build --
+10
diff --git
a/regression-test/suites/nereids_p0/local_shuffle/test_serial_aggregation_over_parallel_join.groovy
b/regression-test/suites/nereids_p0/local_shuffle/test_serial_aggregation_over_parallel_join.groovy
new file mode 100644
index 00000000000..ebe9c6eed63
--- /dev/null
+++
b/regression-test/suites/nereids_p0/local_shuffle/test_serial_aggregation_over_parallel_join.groovy
@@ -0,0 +1,97 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_serial_aggregation_over_parallel_join") {
+ ["serial_agg_join_probe", "serial_agg_join_left",
"serial_agg_join_right"].each { table ->
+ sql "DROP TABLE IF EXISTS ${table}"
+ }
+
+ sql """CREATE TABLE serial_agg_join_probe (
+ col_bigint BIGINT, col_v10 VARCHAR(10), col_v64 VARCHAR(64),
pk INT
+ ) ENGINE=OLAP DISTRIBUTED BY HASH(pk) BUCKETS 10
+ PROPERTIES ("replication_num"="1")"""
+ sql """CREATE TABLE serial_agg_join_left (
+ pk INT, col_bigint BIGINT, col_v10 VARCHAR(10), col_v64
VARCHAR(64)
+ ) ENGINE=OLAP DUPLICATE KEY(pk, col_bigint, col_v10)
+ DISTRIBUTED BY HASH(pk) BUCKETS 10 PROPERTIES
("replication_num"="1")"""
+ sql """CREATE TABLE serial_agg_join_right (
+ pk INT, col_v10 VARCHAR(10), col_bigint BIGINT, col_v64
VARCHAR(64)
+ ) ENGINE=OLAP DUPLICATE KEY(pk, col_v10)
+ DISTRIBUTED BY HASH(pk) BUCKETS 10 PROPERTIES
("replication_num"="1")"""
+
+ sql """INSERT INTO serial_agg_join_probe VALUES
+
(-94,'had','y',0),(672609,'k','h',1),(-3766684,'a','p',2),(5070261,'on','x',3),
+
(NULL,'u','at',4),(-86,'v','c',5),(21910,'how','m',6),(-63,'that''s','go',7),
+ (-8276281,'s','a',8),(-101,'w','y',9)"""
+ sql """INSERT INTO serial_agg_join_left VALUES
+
(0,NULL,'g','i'),(1,-6138328,'z','do'),(2,-23217,'g','about'),(3,104,'you''re','z'),
+
(4,NULL,'oh','i'),(5,-54,'want','to'),(6,NULL,'x','c'),(7,NULL,'you''re','come'),
+ (8,3447,'really','from'),(9,-5459,'i','will')"""
+ sql """INSERT INTO serial_agg_join_right VALUES
+
(0,'right',NULL,'g'),(1,'on',-486256,'on'),(2,'I''ll',-1,'at'),(3,'h',29263,'don''t'),
+
(4,'a',5453,'s'),(5,'j',-119,'can''t'),(6,'one',89,'n'),(7,'s',-7227,'u'),
+ (8,'time',94,'b'),(9,'yes',1816630,'yes')"""
+
+ def variables =
"enable_local_shuffle_planner=true,enable_local_shuffle=true," +
+
"enable_bucket_shuffle_join=true,ignore_storage_data_distribution=true," +
+ "bucket_shuffle_downgrade_ratio=0.8,use_serial_exchange=false," +
+ "parallel_pipeline_task_num=3,enable_sql_cache=false," +
+ "enable_share_hash_table_for_broadcast_join=false"
+
+ order_qt_count_distinct_left_join """SELECT /*+SET_VAR(${variables})*/
COUNT(DISTINCT t1.pk)
+ FROM serial_agg_join_left t1 LEFT JOIN serial_agg_join_probe t2 ON
t2.pk=t1.pk
+ WHERE (t1.col_v64>'FVjnKolDTt' AND t1.col_v64<='z') OR t1.col_v64
IS NULL
+ OR (t1.col_v10>'me' AND t1.col_v10<='zzzz' AND t1.col_bigint
BETWEEN 3 AND 7)"""
+
+ order_qt_sum_and_count_distinct_left_join """SELECT
/*+SET_VAR(${variables})*/
+ SUM(DISTINCT t1.pk), COUNT(DISTINCT t1.pk)
+ FROM serial_agg_join_right t1 LEFT JOIN serial_agg_join_probe t2
ON t2.pk=t1.pk
+ WHERE t1.pk IN (2,9) OR t1.col_bigint IN (1,8)
+ OR (t1.col_v64>='MijtyYyxeA' AND t1.col_v64<'z'
+ AND t1.col_v64>='on' AND t1.col_v64<'zzzz')"""
+
+ order_qt_sum_distinct_broad_predicate """SELECT /*+SET_VAR(${variables})*/
SUM(DISTINCT t1.pk)
+ FROM serial_agg_join_right t1 LEFT JOIN serial_agg_join_probe t2
ON t2.pk=t1.pk
+ WHERE (t1.col_v64>='QXQpaZhWfj' AND t1.col_v64<'z')
+ OR (t1.col_v64>='fvPsFBZelL' AND t1.col_v64<='well')
+ OR (t1.pk BETWEEN 0 AND 15 AND t1.col_v10 LIKE 'a%')
+ OR (t1.pk>=3 AND t1.pk<4) OR t1.pk BETWEEN 0 AND 100 OR
(t1.pk>7 AND t1.pk<=9)"""
+
+ order_qt_sum_distinct_reversed_join """SELECT /*+SET_VAR(${variables})*/
SUM(DISTINCT t1.pk)
+ FROM serial_agg_join_probe t1 LEFT JOIN serial_agg_join_right t2
ON t1.pk=t2.pk
+ WHERE (t1.pk IS NOT NULL AND t1.pk IN (3,8,2,2)
+ AND t1.col_v64 IN
('didn''t','when','a','come','AgpEFIOTAN'))
+ OR (t1.col_v64>'HoatMBMEwP' AND t1.col_v64<='zzzz') OR t1.pk
BETWEEN 6 AND 11
+ OR (t1.pk IS NULL AND t1.pk IN (5)) OR (t1.pk<=t1.col_bigint
AND t1.pk IN (8))"""
+
+ order_qt_sum_distinct_multi_outer_join """SELECT
/*+SET_VAR(${variables})*/ SUM(DISTINCT t1.pk)
+ FROM serial_agg_join_right t1 RIGHT OUTER JOIN
serial_agg_join_probe t2 ON t2.pk=t2.pk
+ LEFT JOIN serial_agg_join_left t3 ON t3.pk=t1.pk
+ WHERE (t1.col_v10>'jHKKlhlHDn' AND t1.col_v10<'z'
+ AND t1.col_v10 NOT IN ('him','you''re'))
+ OR (t1.col_v64>='j' AND t1.col_v64<='y')
+ OR (t1.col_v10 NOT BETWEEN 'rxpMJWfBRX' AND 'z' AND
t1.col_bigint IN (1000)
+ AND t1.col_bigint IS NULL AND t1.col_bigint BETWEEN 6 AND
15)"""
+
+ def nativeVariables =
"enable_local_shuffle_planner=false,enable_local_shuffle=true," +
+ "parallel_pipeline_task_num=3,enable_sql_cache=false," +
+ "enable_share_hash_table_for_broadcast_join=false"
+
+ order_qt_native_private_broadcast_build """SELECT
/*+SET_VAR(${nativeVariables})*/ COUNT(t2.pk)
+ FROM serial_agg_join_right t1 INNER JOIN [broadcast]
serial_agg_join_probe t2 ON t2.pk=t1.pk
+ WHERE t1.pk BETWEEN 0 AND 9"""
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]