This is an automated email from the ASF dual-hosted git repository.
yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new d469cb17507 Support forcing pre-partitioned exchanges for window
functions to avoid data shuffle using query hint (#17395)
d469cb17507 is described below
commit d469cb17507f4e0c35f8bf639272109cb94d9f98
Author: Yash Mayya <[email protected]>
AuthorDate: Thu Jun 18 08:44:55 2026 -0700
Support forcing pre-partitioned exchanges for window functions to avoid
data shuffle using query hint (#17395)
---
.../pinot/calcite/rel/hint/PinotHintOptions.java | 12 +++
.../calcite/rel/hint/PinotHintStrategyTable.java | 1 +
.../rel/logical/PinotLogicalSortExchange.java | 29 +++++--
.../rel/rules/PinotRelDistributionTraitRule.java | 8 ++
.../rules/PinotWindowExchangeNodeInsertRule.java | 8 +-
.../planner/logical/RelToPlanNodeConverter.java | 2 +-
.../apache/pinot/query/QueryCompilationTest.java | 99 ++++++++++++++++++++++
.../resources/queries/ExplainPhysicalPlans.json | 34 ++++++++
.../src/test/resources/queries/QueryHints.json | 12 +++
9 files changed, 196 insertions(+), 9 deletions(-)
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintOptions.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintOptions.java
index 1966ed3dd04..05258e3b000 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintOptions.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintOptions.java
@@ -23,6 +23,7 @@ import javax.annotation.Nullable;
import org.apache.calcite.rel.RelDistribution;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.Window;
import org.apache.calcite.rel.hint.RelHint;
@@ -77,6 +78,17 @@ public class PinotHintOptions {
* BREAK: Break window cache build process, continue to perform WINDOW
operation, results might be partial.
*/
public static final String WINDOW_OVERFLOW_MODE = "window_overflow_mode";
+
+ /// Indicates that the input to the window is already partitioned by the
window keys, so we should avoid shuffling
+ /// to repartition.
+ public static final String IS_PARTITIONED_BY_WINDOW_KEYS =
"is_partitioned_by_window_keys";
+
+ @Nullable
+ public static Boolean isPartitionedByWindowKeys(Window window) {
+ String hint =
+ PinotHintStrategyTable.getHintOption(window.getHints(),
WINDOW_HINT_OPTIONS, IS_PARTITIONED_BY_WINDOW_KEYS);
+ return hint != null ? Boolean.parseBoolean(hint) : null;
+ }
}
public static class JoinHintOptions {
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintStrategyTable.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintStrategyTable.java
index 503dcfe80f6..8837d1d15ce 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintStrategyTable.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/hint/PinotHintStrategyTable.java
@@ -40,6 +40,7 @@ public class PinotHintStrategyTable {
.hintStrategy(PinotHintOptions.AGGREGATE_HINT_OPTIONS,
HintPredicates.AGGREGATE)
.hintStrategy(PinotHintOptions.JOIN_HINT_OPTIONS, HintPredicates.JOIN)
.hintStrategy(PinotHintOptions.TABLE_HINT_OPTIONS,
HintPredicates.TABLE_SCAN)
+ .hintStrategy(PinotHintOptions.WINDOW_HINT_OPTIONS,
HintPredicates.WINDOW)
.build();
/**
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/logical/PinotLogicalSortExchange.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/logical/PinotLogicalSortExchange.java
index 42bd1243390..01457c5e5d9 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/logical/PinotLogicalSortExchange.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/logical/PinotLogicalSortExchange.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.calcite.rel.logical;
+import javax.annotation.Nullable;
import org.apache.calcite.plan.Convention;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelTraitSet;
@@ -45,14 +46,18 @@ public class PinotLogicalSortExchange extends SortExchange {
protected final boolean _isSortOnSender;
protected final boolean _isSortOnReceiver;
protected final PinotRelExchangeType _exchangeType;
+ // Can be used to override the partitioning info calculated from the
distribution trait.
+ @Nullable
+ private final Boolean _prePartitioned;
private PinotLogicalSortExchange(RelOptCluster cluster, RelTraitSet
traitSet, RelNode input,
RelDistribution distribution, PinotRelExchangeType exchangeType,
RelCollation collation, boolean isSortOnSender,
- boolean isSortOnReceiver) {
+ boolean isSortOnReceiver, @Nullable Boolean prePartitioned) {
super(cluster, traitSet, input, distribution, collation);
_exchangeType = exchangeType;
_isSortOnSender = isSortOnSender;
_isSortOnReceiver = isSortOnReceiver;
+ _prePartitioned = prePartitioned;
}
/**
@@ -63,12 +68,19 @@ public class PinotLogicalSortExchange extends SortExchange {
_exchangeType = PinotRelExchangeType.STREAMING;
_isSortOnSender = false;
_isSortOnReceiver = true;
+ _prePartitioned = null;
}
public static PinotLogicalSortExchange create(RelNode input, RelDistribution
distribution, RelCollation collation,
boolean isSortOnSender, boolean isSortOnReceiver) {
return create(input, distribution,
PinotRelExchangeType.getDefaultExchangeType(), collation, isSortOnSender,
- isSortOnReceiver);
+ isSortOnReceiver, null);
+ }
+
+ public static PinotLogicalSortExchange create(RelNode input, RelDistribution
distribution, RelCollation collation,
+ boolean isSortOnSender, boolean isSortOnReceiver, @Nullable Boolean
prePartitioned) {
+ return create(input, distribution,
PinotRelExchangeType.getDefaultExchangeType(), collation, isSortOnSender,
+ isSortOnReceiver, prePartitioned);
}
/**
@@ -80,15 +92,17 @@ public class PinotLogicalSortExchange extends SortExchange {
* @param collation array of sort specifications
* @param isSortOnSender whether to sort on the sender
* @param isSortOnReceiver whether to sort on receiver
+ * @param prePartitioned whether the exchange is pre-partitioned
*/
public static PinotLogicalSortExchange create(RelNode input, RelDistribution
distribution,
- PinotRelExchangeType exchangeType, RelCollation collation, boolean
isSortOnSender, boolean isSortOnReceiver) {
+ PinotRelExchangeType exchangeType, RelCollation collation, boolean
isSortOnSender, boolean isSortOnReceiver,
+ @Nullable Boolean prePartitioned) {
RelOptCluster cluster = input.getCluster();
collation = RelCollationTraitDef.INSTANCE.canonize(collation);
distribution = RelDistributionTraitDef.INSTANCE.canonize(distribution);
RelTraitSet traitSet =
input.getTraitSet().replace(Convention.NONE).replace(distribution).replace(collation);
return new PinotLogicalSortExchange(cluster, traitSet, input,
distribution, exchangeType, collation, isSortOnSender,
- isSortOnReceiver);
+ isSortOnReceiver, prePartitioned);
}
//~ Methods ----------------------------------------------------------------
@@ -97,7 +111,7 @@ public class PinotLogicalSortExchange extends SortExchange {
public SortExchange copy(RelTraitSet traitSet, RelNode newInput,
RelDistribution newDistribution,
RelCollation newCollation) {
return new PinotLogicalSortExchange(this.getCluster(), traitSet, newInput,
newDistribution, _exchangeType,
- newCollation, _isSortOnSender, _isSortOnReceiver);
+ newCollation, _isSortOnSender, _isSortOnReceiver, _prePartitioned);
}
@Override
@@ -121,4 +135,9 @@ public class PinotLogicalSortExchange extends SortExchange {
public PinotRelExchangeType getExchangeType() {
return _exchangeType;
}
+
+ @Nullable
+ public Boolean getPrePartitioned() {
+ return _prePartitioned;
+ }
}
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRelDistributionTraitRule.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRelDistributionTraitRule.java
index 978986c5e16..a77780f01ee 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRelDistributionTraitRule.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRelDistributionTraitRule.java
@@ -36,6 +36,7 @@ import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.logical.LogicalFilter;
import org.apache.calcite.rel.logical.LogicalJoin;
import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.logical.LogicalWindow;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.tools.RelBuilderFactory;
import org.apache.calcite.util.mapping.IntPair;
@@ -158,6 +159,13 @@ public class PinotRelDistributionTraitRule extends
RelOptRule {
// b/c the Join node always puts left relation RowTypes then right
relation RowTypes sequentially.
return inputRelDistribution;
}
+ } else if (node instanceof LogicalWindow) {
+ assert inputs.size() == 1;
+ // Window input should be an exchange node that is hash distributed by
the window's partition keys
+ RelDistribution inputRelDistribution =
inputs.get(0).getTraitSet().getDistribution();
+ if (inputRelDistribution != null) {
+ return inputRelDistribution;
+ }
}
// TODO: add the rest of the nodes.
return computeCurrentDistribution(node);
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotWindowExchangeNodeInsertRule.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotWindowExchangeNodeInsertRule.java
index 5eaade4125f..f8f3e0978b9 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotWindowExchangeNodeInsertRule.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotWindowExchangeNodeInsertRule.java
@@ -43,6 +43,7 @@ import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.pinot.calcite.rel.hint.PinotHintOptions;
import org.apache.pinot.calcite.rel.logical.PinotLogicalExchange;
import org.apache.pinot.calcite.rel.logical.PinotLogicalSortExchange;
@@ -119,11 +120,12 @@ public class PinotWindowExchangeNodeInsertRule extends
RelOptRule {
// Assess whether this is a PARTITION BY only query or not (includes
queries of the type where PARTITION BY and
// ORDER BY key(s) are the same)
boolean isPartitionByOnly = isPartitionByOnlyQuery(windowGroup);
-
+ // Force pre-partitioned exchange when 'is_partitioned_by_window_keys'
hint is provided
+ Boolean prePartitioned =
PinotHintOptions.WindowHintOptions.isPartitionedByWindowKeys(window);
if (isPartitionByOnly) {
// Only PARTITION BY or PARTITION BY and ORDER BY on the same key(s)
// Add an Exchange hashed on the partition by keys
- exchange = PinotLogicalExchange.create(input,
RelDistributions.hash(windowGroup.keys.toList()));
+ exchange = PinotLogicalExchange.create(input,
RelDistributions.hash(windowGroup.keys.toList()), prePartitioned);
} else {
// PARTITION BY and ORDER BY on different key(s)
// Add a LogicalSortExchange hashed on the partition by keys and
collation based on order by keys
@@ -132,7 +134,7 @@ public class PinotWindowExchangeNodeInsertRule extends
RelOptRule {
// sorting on the receiver side can be a no-op. Add support for
this hint and pass it on. Until sender
// side sorting is implemented, setting this hint will throw an
error on execution.
exchange = PinotLogicalSortExchange.create(input,
RelDistributions.hash(windowGroup.keys.toList()),
- windowGroup.orderKeys, false, true);
+ windowGroup.orderKeys, false, true, prePartitioned);
}
}
// NOTE: Need to create a new LogicalWindow to use the modified window
group.
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RelToPlanNodeConverter.java
b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RelToPlanNodeConverter.java
index cd737896c73..c347e18a79d 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RelToPlanNodeConverter.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RelToPlanNodeConverter.java
@@ -581,7 +581,7 @@ public final class RelToPlanNodeConverter {
PinotLogicalSortExchange sortExchange = (PinotLogicalSortExchange) node;
exchangeType = sortExchange.getExchangeType();
keys = distribution.getKeys();
- prePartitioned = null;
+ prePartitioned = sortExchange.getPrePartitioned();
collations = sortExchange.getCollation().getFieldCollations();
sortOnSender = sortExchange.isSortOnSender();
sortOnReceiver = sortExchange.isSortOnReceiver();
diff --git
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
index 142eddcb05b..1bf31abd431 100644
---
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
+++
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
@@ -38,8 +38,10 @@ import org.apache.pinot.query.planner.plannode.BasePlanNode;
import org.apache.pinot.query.planner.plannode.FilterNode;
import org.apache.pinot.query.planner.plannode.JoinNode;
import org.apache.pinot.query.planner.plannode.MailboxReceiveNode;
+import org.apache.pinot.query.planner.plannode.MailboxSendNode;
import org.apache.pinot.query.planner.plannode.PlanNode;
import org.apache.pinot.query.planner.plannode.ProjectNode;
+import org.apache.pinot.query.planner.plannode.WindowNode;
import org.apache.pinot.query.routing.QueryServerInstance;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Ignore;
@@ -953,6 +955,103 @@ public class QueryCompilationTest extends
QueryEnvironmentTestBase {
"RIGHT side of RIGHT non-equi join should be RANDOM");
}
+ /**
+ * The {@code windowOptions(is_partitioned_by_window_keys='true')} hint
forces a pre-partitioned (direct) exchange
+ * below the window, avoiding a data shuffle. Here the window partitions by
{@code col1}, which is NOT table a's
+ * partition column ({@code col2}), so without the hint the planner would
shuffle. This exercises the
+ * {@link org.apache.pinot.calcite.rel.logical.PinotLogicalExchange} path
(PARTITION BY only).
+ */
+ @Test
+ public void testWindowPartitionByKeysHintForcesPrePartitionedExchange() {
+ String query = "SELECT /*+
windowOptions(is_partitioned_by_window_keys='true') */ "
+ + "col1, SUM(col3) OVER (PARTITION BY col1) FROM a";
+ MailboxSendNode sendNode =
findWindowInputSendNode(_queryEnvironment.planQuery(query));
+ assertEquals(sendNode.getDistributionType(),
RelDistribution.Type.HASH_DISTRIBUTED);
+ assertTrue(sendNode.isPrePartitioned(),
+ "windowOptions(is_partitioned_by_window_keys='true') should force a
pre-partitioned exchange");
+ }
+
+ /**
+ * Without the hint and with a window partition key that does not match the
table's partitioning, the exchange below
+ * the window must be a regular (shuffled) exchange.
+ */
+ @Test
+ public void testWindowWithoutHintIsNotPrePartitioned() {
+ String query = "SELECT col1, SUM(col3) OVER (PARTITION BY col1) FROM a";
+ MailboxSendNode sendNode =
findWindowInputSendNode(_queryEnvironment.planQuery(query));
+ assertFalse(sendNode.isPrePartitioned(),
+ "Without the hint and matching partitioning, the window exchange
should be a full shuffle");
+ }
+
+ /**
+ * The hint must also flow through the {@link
org.apache.pinot.calcite.rel.logical.PinotLogicalSortExchange} path,
+ * used when PARTITION BY and ORDER BY are on different keys. This is the
path the PR fixes: previously
+ * {@code RelToPlanNodeConverter} hardcoded {@code prePartitioned = null}
for sort exchanges, dropping the hint.
+ */
+ @Test
+ public void testWindowPartitionByKeysHintForcesPrePartitionedSortExchange() {
+ String query = "SELECT /*+
windowOptions(is_partitioned_by_window_keys='true') */ "
+ + "col1, SUM(col3) OVER (PARTITION BY col1 ORDER BY col3) FROM a";
+ MailboxSendNode sendNode =
findWindowInputSendNode(_queryEnvironment.planQuery(query));
+ assertEquals(sendNode.getDistributionType(),
RelDistribution.Type.HASH_DISTRIBUTED);
+ assertTrue(sendNode.isPrePartitioned(),
+ "windowOptions hint should force a pre-partitioned sort exchange
(PARTITION BY + ORDER BY on different keys)");
+ }
+
+ /**
+ * Setting the hint to {@code 'false'} overrides the planner's automatic
detection of pre-partitioning. Here table a
+ * is declared partitioned by {@code col2} (via tableOptions) and the window
also partitions by {@code col2}, so the
+ * planner would otherwise auto-detect a pre-partitioned exchange; the hint
disables it.
+ */
+ @Test
+ public void
testWindowPartitionByKeysHintFalseDisablesAutoDetectedPrePartitioning() {
+ String query = "SELECT /*+
windowOptions(is_partitioned_by_window_keys='false') */ "
+ + "col1, SUM(col3) OVER (PARTITION BY col2) "
+ + "FROM a /*+ tableOptions(partition_function='hashcode',
partition_key='col2', partition_size='4') */";
+ MailboxSendNode sendNode =
findWindowInputSendNode(_queryEnvironment.planQuery(query));
+ assertFalse(sendNode.isPrePartitioned(),
+ "windowOptions(is_partitioned_by_window_keys='false') should disable
auto-detected pre-partitioning");
+ }
+
+ /**
+ * With matching tableOptions partitioning and no window hint, the planner
auto-detects pre-partitioning. This must
+ * hold for both window exchange paths: PinotLogicalExchange (PARTITION BY
only) and PinotLogicalSortExchange
+ * (PARTITION BY + ORDER BY). The latter verifies the PR preserves the
{@code null -> auto-detect} behavior.
+ */
+ @Test
+ public void testWindowAutoDetectsPrePartitioningWithoutHint() {
+ String partitionOnly = "SELECT col1, SUM(col3) OVER (PARTITION BY col2) "
+ + "FROM a /*+ tableOptions(partition_function='hashcode',
partition_key='col2', partition_size='4') */";
+
assertTrue(findWindowInputSendNode(_queryEnvironment.planQuery(partitionOnly)).isPrePartitioned(),
+ "PARTITION BY on the table's partition column should auto-detect a
pre-partitioned exchange");
+
+ String partitionAndOrder = "SELECT col1, SUM(col3) OVER (PARTITION BY col2
ORDER BY col3) "
+ + "FROM a /*+ tableOptions(partition_function='hashcode',
partition_key='col2', partition_size='4') */";
+
assertTrue(findWindowInputSendNode(_queryEnvironment.planQuery(partitionAndOrder)).isPrePartitioned(),
+ "Sort exchange should also auto-detect pre-partitioning when the table
is partitioned by the window key");
+ }
+
+ /**
+ * Finds the {@link MailboxSendNode} that feeds the (single) WINDOW stage's
input exchange, i.e. the sender side of
+ * the exchange inserted directly below the window. The {@code
prePartitioned} flag lives on this send node.
+ */
+ private MailboxSendNode findWindowInputSendNode(DispatchableSubPlan
dispatchableSubPlan) {
+ WindowNode window = null;
+ for (DispatchablePlanFragment fragment :
dispatchableSubPlan.getQueryStages()) {
+ window = findNodeOfType(fragment.getPlanFragment().getFragmentRoot(),
WindowNode.class);
+ if (window != null) {
+ break;
+ }
+ }
+ assertNotNull(window, "Expected a WINDOW node in the plan");
+ MailboxReceiveNode receiveNode = findNodeOfType(window,
MailboxReceiveNode.class);
+ assertNotNull(receiveNode, "Expected the WINDOW input to be a mailbox
exchange");
+ PlanNode senderRoot =
+
dispatchableSubPlan.getQueryStageMap().get(receiveNode.getSenderStageId()).getPlanFragment().getFragmentRoot();
+ assertTrue(senderRoot instanceof MailboxSendNode, "Sender fragment root
should be a MailboxSendNode");
+ return (MailboxSendNode) senderRoot;
+ }
+
/**
* Finds the DispatchablePlanFragment containing a JoinNode (non-leaf,
non-root stage).
*/
diff --git
a/pinot-query-planner/src/test/resources/queries/ExplainPhysicalPlans.json
b/pinot-query-planner/src/test/resources/queries/ExplainPhysicalPlans.json
index b64b2da593d..55535941b97 100644
--- a/pinot-query-planner/src/test/resources/queries/ExplainPhysicalPlans.json
+++ b/pinot-query-planner/src/test/resources/queries/ExplainPhysicalPlans.json
@@ -716,6 +716,40 @@
"\n └── [5]@localhost:1|[0] TABLE
SCAN (b) null",
"\n"
]
+ },
+ {
+ "description": "window function (PARTITION BY) without hint shuffles
(hash redistributes) its input to all window-stage workers",
+ "sql": "EXPLAIN IMPLEMENTATION PLAN FOR SELECT col1, SUM(col3) OVER
(PARTITION BY col1) FROM a",
+ "output": [
+ "[0]@localhost:3|[0] MAIL_RECEIVE(BROADCAST_DISTRIBUTED)",
+ "\n├── [1]@localhost:2|[1]
MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@localhost:3|[0]} (Subtree Omitted)",
+ "\n└── [1]@localhost:1|[0]
MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@localhost:3|[0]}",
+ "\n └── [1]@localhost:1|[0] PROJECT",
+ "\n └── [1]@localhost:1|[0] WINDOW",
+ "\n └── [1]@localhost:1|[0]
MAIL_RECEIVE(HASH_DISTRIBUTED)",
+ "\n ├── [2]@localhost:2|[1]
MAIL_SEND(HASH_DISTRIBUTED)->{[1]@localhost:1|[0],[1]@localhost:2|[1]} (Subtree
Omitted)",
+ "\n └── [2]@localhost:1|[0]
MAIL_SEND(HASH_DISTRIBUTED)->{[1]@localhost:1|[0],[1]@localhost:2|[1]}",
+ "\n └── [2]@localhost:1|[0] PROJECT",
+ "\n └── [2]@localhost:1|[0] TABLE SCAN (a)
null",
+ "\n"
+ ]
+ },
+ {
+ "description": "window function (PARTITION BY) with
is_partitioned_by_window_keys hint uses a pre-partitioned (direct, 1-to-1)
exchange to avoid the shuffle",
+ "sql": "EXPLAIN IMPLEMENTATION PLAN FOR SELECT /*+
windowOptions(is_partitioned_by_window_keys='true') */ col1, SUM(col3) OVER
(PARTITION BY col1) FROM a",
+ "output": [
+ "[0]@localhost:3|[0] MAIL_RECEIVE(BROADCAST_DISTRIBUTED)",
+ "\n├── [1]@localhost:2|[1]
MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@localhost:3|[0]} (Subtree Omitted)",
+ "\n└── [1]@localhost:1|[0]
MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@localhost:3|[0]}",
+ "\n └── [1]@localhost:1|[0] PROJECT",
+ "\n └── [1]@localhost:1|[0] WINDOW",
+ "\n └── [1]@localhost:1|[0]
MAIL_RECEIVE(HASH_DISTRIBUTED)",
+ "\n ├── [2]@localhost:2|[1]
MAIL_SEND(HASH_DISTRIBUTED)[PARTITIONED]->{[1]@localhost:2|[1]} (Subtree
Omitted)",
+ "\n └── [2]@localhost:1|[0]
MAIL_SEND(HASH_DISTRIBUTED)[PARTITIONED]->{[1]@localhost:1|[0]}",
+ "\n └── [2]@localhost:1|[0] PROJECT",
+ "\n └── [2]@localhost:1|[0] TABLE SCAN (a)
null",
+ "\n"
+ ]
}
]
}
diff --git a/pinot-query-runtime/src/test/resources/queries/QueryHints.json
b/pinot-query-runtime/src/test/resources/queries/QueryHints.json
index 2af747c2704..8b8bcd6f0bb 100644
--- a/pinot-query-runtime/src/test/resources/queries/QueryHints.json
+++ b/pinot-query-runtime/src/test/resources/queries/QueryHints.json
@@ -659,6 +659,18 @@
{
"description": "Local replicated JOIN with partition hint and
parallelism",
"sql": "SELECT /*+ joinOptions(left_distribution_type = 'local',
right_distribution_type = 'local') */ {tbl1}.num, {tbl1}.name, {tbl2}.num,
{tbl2}.val FROM {tbl1} /*+ tableOptions(partition_function='hashcode',
partition_key='num', partition_size='4', partition_parallelism='2') */ JOIN
{tbl2} /*+ tableOptions(is_replicated='true') */ ON {tbl1}.num = {tbl2}.num"
+ },
+ {
+ "description": "Window PARTITION BY the table's partition column with
is_partitioned_by_window_keys hint forces a pre-partitioned (direct) exchange;
tbl1 is partitioned by num so results stay correct",
+ "sql": "SELECT /*+ windowOptions(is_partitioned_by_window_keys='true')
*/ {tbl1}.num, SUM({tbl1}.num) OVER (PARTITION BY {tbl1}.num) FROM {tbl1}"
+ },
+ {
+ "description": "Window PARTITION BY + ORDER BY on different keys with
is_partitioned_by_window_keys hint forces a pre-partitioned sort exchange; tbl1
is partitioned by num so results stay correct",
+ "sql": "SELECT /*+ windowOptions(is_partitioned_by_window_keys='true')
*/ {tbl1}.num, {tbl1}.name, SUM({tbl1}.num) OVER (PARTITION BY {tbl1}.num ORDER
BY {tbl1}.name) FROM {tbl1}"
+ },
+ {
+ "description": "Window with is_partitioned_by_window_keys='false'
disables the auto-detected pre-partitioning (tbl1 is partitioned by num) and
falls back to a shuffle; results stay correct",
+ "sql": "SELECT /*+ windowOptions(is_partitioned_by_window_keys='false')
*/ {tbl1}.num, SUM({tbl1}.num) OVER (PARTITION BY {tbl1}.num) FROM {tbl1} /*+
tableOptions(partition_function='hashcode', partition_key='num',
partition_size='4') */"
}
]}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]