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 f865accd778 Support forcing colocated set operations to avoid data 
shuffle using query hint (#18804)
f865accd778 is described below

commit f865accd77820505953cfbb106523b34ef5c9a5e
Author: Yash Mayya <[email protected]>
AuthorDate: Thu Jul 16 11:11:33 2026 -0700

    Support forcing colocated set operations to avoid data shuffle using query 
hint (#18804)
---
 .../pinot/calcite/rel/hint/PinotHintOptions.java   |  28 +++++
 .../calcite/rel/hint/PinotHintStrategyTable.java   |   1 +
 .../rules/PinotSetOpExchangeNodeInsertRule.java    |  32 +++++-
 .../apache/pinot/query/QueryCompilationTest.java   | 115 +++++++++++++++++++++
 .../resources/queries/ExplainPhysicalPlans.json    |  42 ++++++++
 .../src/test/resources/queries/QueryHints.json     |  25 +++++
 6 files changed, 242 insertions(+), 1 deletion(-)

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 05258e3b000..dcdf30d6235 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
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.calcite.rel.hint;
 
+import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
 import org.apache.calcite.rel.RelDistribution;
@@ -42,6 +43,7 @@ public class PinotHintOptions {
   public static final String JOIN_HINT_OPTIONS = "joinOptions";
   public static final String TABLE_HINT_OPTIONS = "tableOptions";
   public static final String WINDOW_HINT_OPTIONS = "windowOptions";
+  public static final String SET_OP_HINT_OPTIONS = "setOpOptions";
 
   public static class AggregateOptions {
     public static final String IS_PARTITIONED_BY_GROUP_BY_KEYS = 
"is_partitioned_by_group_by_keys";
@@ -91,6 +93,32 @@ public class PinotHintOptions {
     }
   }
 
+  /// Hint options for set operations (UNION / UNION ALL / INTERSECT / EXCEPT).
+  public static class SetOpHintOptions {
+    /// Forces (or disables) a colocated, pre-partitioned exchange on every 
input of a set operation
+    /// (UNION / UNION ALL / INTERSECT / EXCEPT), so the inputs are processed 
in place without a network shuffle.
+    ///
+    /// This is opt-in and honored only by the default (V1) query planner; the 
V2 physical planner determines
+    /// colocation on its own. Like the join hint {@link 
JoinHintOptions#IS_COLOCATED_BY_JOIN_KEYS}, it trusts the user:
+    /// {@code 'true'} forces a pre-partitioned exchange and bypasses the 
planner's automatic detection, while
+    /// {@code 'false'} forces a normal shuffle even when the planner would 
otherwise detect pre-partitioning. A set
+    /// operation matches rows on the entire output row, so forcing {@code 
'true'} is only correct when all inputs are
+    /// partitioned the same way (same partition function and count) on one or 
more of the projected columns, so that
+    /// rows which are equal across all projected columns are guaranteed to 
land on the same worker. Forcing it when
+    /// that does not hold silently produces wrong results for INTERSECT, 
EXCEPT and distinct UNION; UNION ALL only
+    /// concatenates and is unaffected.
+    public static final String IS_COLOCATED_BY_SET_OP_KEYS = 
"is_colocated_by_set_op_keys";
+
+    /// Reads the hint from a hint list. Unlike {@link 
JoinHintOptions#isColocatedByJoinKeys}, this takes the hint list
+    /// directly because the hint can attach either to the set operation 
itself or to one of its inputs (see
+    /// {@code PinotSetOpExchangeNodeInsertRule}), and the caller resolves 
which one applies.
+    @Nullable
+    public static Boolean isColocatedBySetOpKeys(List<RelHint> hints) {
+      String hint = PinotHintStrategyTable.getHintOption(hints, 
SET_OP_HINT_OPTIONS, IS_COLOCATED_BY_SET_OP_KEYS);
+      return hint != null ? Boolean.parseBoolean(hint) : null;
+    }
+  }
+
   public static class JoinHintOptions {
     public static final String JOIN_STRATEGY = "join_strategy";
     // "hash" is the default strategy for non-SEMI joins
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 8837d1d15ce..79dc6e5ca2c 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
@@ -41,6 +41,7 @@ public class PinotHintStrategyTable {
       .hintStrategy(PinotHintOptions.JOIN_HINT_OPTIONS, HintPredicates.JOIN)
       .hintStrategy(PinotHintOptions.TABLE_HINT_OPTIONS, 
HintPredicates.TABLE_SCAN)
       .hintStrategy(PinotHintOptions.WINDOW_HINT_OPTIONS, 
HintPredicates.WINDOW)
+      .hintStrategy(PinotHintOptions.SET_OP_HINT_OPTIONS, HintPredicates.SETOP)
       .build();
 
   /**
diff --git 
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotSetOpExchangeNodeInsertRule.java
 
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotSetOpExchangeNodeInsertRule.java
index 3db58c048c0..7ecfac362f0 100644
--- 
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotSetOpExchangeNodeInsertRule.java
+++ 
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotSetOpExchangeNodeInsertRule.java
@@ -20,13 +20,16 @@ package org.apache.pinot.calcite.rel.rules;
 
 import java.util.ArrayList;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.rel.RelDistributions;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.SetOp;
+import org.apache.calcite.rel.hint.Hintable;
 import org.apache.calcite.tools.RelBuilderFactory;
 import org.apache.calcite.util.ImmutableIntList;
+import org.apache.pinot.calcite.rel.hint.PinotHintOptions;
 import org.apache.pinot.calcite.rel.logical.PinotLogicalExchange;
 
 
@@ -51,12 +54,39 @@ public class PinotSetOpExchangeNodeInsertRule extends 
RelOptRule {
   public void onMatch(RelOptRuleCall call) {
     SetOp setOp = call.rel(0);
     List<RelNode> inputs = setOp.getInputs();
+    // When the colocation hint is set, force a pre-partitioned (direct, 
no-shuffle) exchange on every input; otherwise
+    // leave it null so the planner auto-detects pre-partitioning from the 
inputs' distribution. See
+    // PinotHintOptions.SetOpHintOptions.IS_COLOCATED_BY_SET_OP_KEYS for the 
correctness contract.
+    Boolean prePartitioned = resolveColocationHint(setOp);
     List<RelNode> newInputs = new ArrayList<>(inputs.size());
     for (RelNode input : inputs) {
       RelNode exchange = PinotLogicalExchange.create(input,
-          RelDistributions.hash(ImmutableIntList.range(0, 
setOp.getRowType().getFieldCount())));
+          RelDistributions.hash(ImmutableIntList.range(0, 
setOp.getRowType().getFieldCount())), prePartitioned);
       newInputs.add(exchange);
     }
     call.transformTo(setOp.copy(setOp.getTraitSet(), newInputs));
   }
+
+  /// Resolves the colocation hint for a set operation. Calcite attaches the 
hint to the set operation itself only when
+  /// the query wraps it in an outer {@code SELECT} that carries the hint; in 
the natural inline form (the hint on the
+  /// first {@code SELECT} of a {@code UNION}/{@code INTERSECT}/{@code 
EXCEPT}) it lands on the first branch instead.
+  /// Precedence: the set operation's own hint wins, otherwise the first input 
carrying the hint wins. The resolved
+  /// value is applied to all inputs, so conflicting per-branch values are not 
supported.
+  @Nullable
+  private static Boolean resolveColocationHint(SetOp setOp) {
+    Boolean fromSetOp = 
PinotHintOptions.SetOpHintOptions.isColocatedBySetOpKeys(setOp.getHints());
+    if (fromSetOp != null) {
+      return fromSetOp;
+    }
+    for (RelNode input : setOp.getInputs()) {
+      RelNode unboxed = PinotRuleUtils.unboxRel(input);
+      if (unboxed instanceof Hintable) {
+        Boolean fromInput = 
PinotHintOptions.SetOpHintOptions.isColocatedBySetOpKeys(((Hintable) 
unboxed).getHints());
+        if (fromInput != null) {
+          return fromInput;
+        }
+      }
+    }
+    return null;
+  }
 }
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 1bf31abd431..1512b0e5558 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
@@ -41,6 +41,7 @@ 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.SetOpNode;
 import org.apache.pinot.query.planner.plannode.WindowNode;
 import org.apache.pinot.query.routing.QueryServerInstance;
 import org.testng.annotations.DataProvider;
@@ -1052,6 +1053,120 @@ public class QueryCompilationTest extends 
QueryEnvironmentTestBase {
     return (MailboxSendNode) senderRoot;
   }
 
+  /// The {@code setOpOptions(is_colocated_by_set_op_keys='true')} hint forces 
a pre-partitioned (direct) exchange on
+  /// every input of a set operation, avoiding the shuffle. Here the inputs 
project {@code col3}, which is neither
+  /// table's partition column, so without the hint the planner would shuffle. 
Covers UNION ALL, INTERSECT and EXCEPT,
+  /// which all share {@link 
org.apache.pinot.calcite.rel.rules.PinotSetOpExchangeNodeInsertRule}.
+  @Test(dataProvider = "setOpColocationHintQueries")
+  public void testSetOpColocationHintForcesPrePartitionedExchange(String 
query) {
+    List<MailboxSendNode> sendNodes = 
findSetOpInputSendNodes(_queryEnvironment.planQuery(query));
+    for (MailboxSendNode sendNode : sendNodes) {
+      assertEquals(sendNode.getDistributionType(), 
RelDistribution.Type.HASH_DISTRIBUTED);
+      assertTrue(sendNode.isPrePartitioned(),
+          "setOpOptions(is_colocated_by_set_op_keys='true') should force a 
pre-partitioned exchange on every input");
+    }
+  }
+
+  @DataProvider(name = "setOpColocationHintQueries")
+  private Object[][] setOpColocationHintQueries() {
+    String hint = "/*+ setOpOptions(is_colocated_by_set_op_keys='true') */";
+    return new Object[][]{
+        {"SELECT " + hint + " col3 FROM a UNION ALL SELECT col3 FROM b"},
+        {"SELECT " + hint + " col3 FROM a INTERSECT SELECT col3 FROM b"},
+        {"SELECT " + hint + " col3 FROM a EXCEPT SELECT col3 FROM b"},
+    };
+  }
+
+  /// The hint also lands on the set operation when it is wrapped in an outer 
{@code SELECT} that carries the hint (the
+  /// hint then attaches to the set operation directly rather than to a 
branch).
+  @Test
+  public void testSetOpColocationHintViaOuterSelectWrap() {
+    String query = "SELECT /*+ 
setOpOptions(is_colocated_by_set_op_keys='true') */ * FROM "
+        + "(SELECT col3 FROM a UNION ALL SELECT col3 FROM b)";
+    for (MailboxSendNode sendNode : 
findSetOpInputSendNodes(_queryEnvironment.planQuery(query))) {
+      assertTrue(sendNode.isPrePartitioned(), "Hint on the wrapping SELECT 
should force a pre-partitioned exchange");
+    }
+  }
+
+  /// When branches carry conflicting hints, the first input that specifies 
the hint wins and its value is applied to
+  /// all inputs (the documented precedence of the rule). Here the first 
branch forces it on, so both branches are
+  /// pre-partitioned even though the second branch sets it to {@code 'false'}.
+  @Test
+  public void testSetOpColocationHintFirstInputWins() {
+    String query = "SELECT /*+ 
setOpOptions(is_colocated_by_set_op_keys='true') */ col3 FROM a "
+        + "UNION ALL SELECT /*+ 
setOpOptions(is_colocated_by_set_op_keys='false') */ col3 FROM a";
+    for (MailboxSendNode sendNode : 
findSetOpInputSendNodes(_queryEnvironment.planQuery(query))) {
+      assertTrue(sendNode.isPrePartitioned(),
+          "The first input's hint value should win and apply to all inputs 
when branches conflict");
+    }
+  }
+
+  /// Without the hint and with inputs that are not partitioned by the 
projected column, the exchanges below the set
+  /// operation must be regular (shuffled) exchanges.
+  @Test
+  public void testSetOpWithoutHintIsNotPrePartitioned() {
+    String query = "SELECT col3 FROM a UNION ALL SELECT col3 FROM b";
+    for (MailboxSendNode sendNode : 
findSetOpInputSendNodes(_queryEnvironment.planQuery(query))) {
+      assertFalse(sendNode.isPrePartitioned(),
+          "Without the hint and matching partitioning, the set op exchanges 
should be a full shuffle");
+    }
+  }
+
+  /// When each input is declared partitioned by the projected column, the 
single-column set-op exchange matches the
+  /// input partitioning, so the planner auto-detects a pre-partitioned 
exchange even without the hint. This is the
+  /// baseline that {@link #testSetOpColocationHintFalseDisablesAutoDetected} 
overrides.
+  @Test
+  public void testSetOpAutoDetectsPrePartitioningWithoutHint() {
+    for (MailboxSendNode sendNode : 
findSetOpInputSendNodes(_queryEnvironment.planQuery(AUTO_DETECTED_SET_OP))) {
+      assertTrue(sendNode.isPrePartitioned(),
+          "A single-column set op over each table's partition column should 
auto-detect a pre-partitioned exchange");
+    }
+  }
+
+  /// Setting the hint to {@code 'false'} overrides the planner's automatic 
detection of pre-partitioning (see
+  /// {@link #testSetOpAutoDetectsPrePartitioningWithoutHint} for the same 
query without the hint).
+  @Test
+  public void testSetOpColocationHintFalseDisablesAutoDetected() {
+    String query = AUTO_DETECTED_SET_OP.replaceFirst("SELECT",
+        "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='false') */");
+    for (MailboxSendNode sendNode : 
findSetOpInputSendNodes(_queryEnvironment.planQuery(query))) {
+      assertFalse(sendNode.isPrePartitioned(),
+          "setOpOptions(is_colocated_by_set_op_keys='false') should disable 
auto-detected pre-partitioning");
+    }
+  }
+
+  // A set op whose inputs are each declared partitioned by the (single) 
projected column, so the exchange below the set
+  // op matches the input partitioning and the planner auto-detects 
pre-partitioning.
+  private static final String AUTO_DETECTED_SET_OP =
+      "SELECT col2 FROM a /*+ tableOptions(partition_function='hashcode', 
partition_key='col2', partition_size='4') */ "
+          + "UNION ALL "
+          + "SELECT col1 FROM b /*+ 
tableOptions(partition_function='hashcode', partition_key='col1', "
+          + "partition_size='4') */";
+
+  /// Finds the {@link MailboxSendNode}s feeding each input exchange of the 
(single) set-op stage. A set operation has
+  /// one mailbox exchange per branch, and the {@code prePartitioned} flag 
lives on each branch's send node.
+  private List<MailboxSendNode> findSetOpInputSendNodes(DispatchableSubPlan 
dispatchableSubPlan) {
+    SetOpNode setOpNode = null;
+    for (DispatchablePlanFragment fragment : 
dispatchableSubPlan.getQueryStages()) {
+      setOpNode = findNodeOfType(fragment.getPlanFragment().getFragmentRoot(), 
SetOpNode.class);
+      if (setOpNode != null) {
+        break;
+      }
+    }
+    assertNotNull(setOpNode, "Expected a SetOp node in the plan");
+    List<MailboxSendNode> sendNodes = new ArrayList<>();
+    for (PlanNode input : setOpNode.getInputs()) {
+      assertTrue(input instanceof MailboxReceiveNode, "Each SetOp input should 
be a mailbox exchange");
+      int senderStageId = ((MailboxReceiveNode) input).getSenderStageId();
+      PlanNode senderRoot =
+          
dispatchableSubPlan.getQueryStageMap().get(senderStageId).getPlanFragment().getFragmentRoot();
+      assertTrue(senderRoot instanceof MailboxSendNode, "Sender fragment root 
should be a MailboxSendNode");
+      sendNodes.add((MailboxSendNode) senderRoot);
+    }
+    assertFalse(sendNodes.isEmpty(), "Expected the SetOp to have input send 
nodes");
+    return sendNodes;
+  }
+
   /**
    * 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 641603e9468..20ef9aed09e 100644
--- a/pinot-query-planner/src/test/resources/queries/ExplainPhysicalPlans.json
+++ b/pinot-query-planner/src/test/resources/queries/ExplainPhysicalPlans.json
@@ -741,6 +741,48 @@
           "\n                        └── [2]@localhost:1|[0] TABLE SCAN (a) 
null",
           "\n"
         ]
+      },
+      {
+        "description": "set op (UNION ALL) without the colocation hint 
shuffles (hash redistributes) its inputs to all set-op-stage workers",
+        "sql": "EXPLAIN IMPLEMENTATION PLAN FOR SELECT col3 FROM a UNION ALL 
SELECT col3 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] UNION_ALL",
+          "\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        └── [1]@localhost:1|[0] MAIL_RECEIVE(HASH_DISTRIBUTED)",
+          "\n            ├── [3]@localhost:2|[1] 
MAIL_SEND(HASH_DISTRIBUTED)->{[1]@localhost:1|[0],[1]@localhost:2|[1]} (Subtree 
Omitted)",
+          "\n            └── [3]@localhost:1|[0] 
MAIL_SEND(HASH_DISTRIBUTED)->{[1]@localhost:1|[0],[1]@localhost:2|[1]}",
+          "\n                └── [3]@localhost:1|[0] PROJECT",
+          "\n                    └── [3]@localhost:1|[0] TABLE SCAN (a) null",
+          "\n"
+        ]
+      },
+      {
+        "description": "set op (UNION ALL) with the 
is_colocated_by_set_op_keys hint uses a pre-partitioned (direct, 1-to-1) 
exchange on every input to avoid the shuffle",
+        "sql": "EXPLAIN IMPLEMENTATION PLAN FOR SELECT /*+ 
setOpOptions(is_colocated_by_set_op_keys='true') */ col3 FROM a UNION ALL 
SELECT col3 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] UNION_ALL",
+          "\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        └── [1]@localhost:1|[0] MAIL_RECEIVE(HASH_DISTRIBUTED)",
+          "\n            ├── [3]@localhost:2|[1] 
MAIL_SEND(HASH_DISTRIBUTED)[PARTITIONED]->{[1]@localhost:2|[1]} (Subtree 
Omitted)",
+          "\n            └── [3]@localhost:1|[0] 
MAIL_SEND(HASH_DISTRIBUTED)[PARTITIONED]->{[1]@localhost:1|[0]}",
+          "\n                └── [3]@localhost:1|[0] PROJECT",
+          "\n                    └── [3]@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 8b8bcd6f0bb..c8f872bd3ee 100644
--- a/pinot-query-runtime/src/test/resources/queries/QueryHints.json
+++ b/pinot-query-runtime/src/test/resources/queries/QueryHints.json
@@ -162,6 +162,27 @@
       {
         "description": "Colocated, Dynamic broadcast SEMI-JOIN with partially 
empty right table result for some servers",
         "sql": "SELECT /*+ joinOptions(join_strategy='dynamic_broadcast') */ 
{tbl1}.name, COUNT(*) FROM {tbl1} /*+ 
tableOptions(partition_function='hashcode', partition_key='num', 
partition_size='4') */ WHERE {tbl1}.num IN (SELECT {tbl2}.num FROM {tbl2} /*+ 
tableOptions(partition_function='hashcode', partition_key='num', 
partition_size='4') */ WHERE {tbl2}.val = 'z') GROUP BY {tbl1}.name"
+      },
+      {
+        "description": "INTERSECT on the partition column num with 
is_colocated_by_set_op_keys hint forces a pre-partitioned (direct) exchange; 
tbl1 and tbl2 are both partitioned by num so results stay correct",
+        "sql": "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='true') */ 
{tbl1}.num FROM {tbl1} INTERSECT SELECT {tbl2}.num FROM {tbl2}"
+      },
+      {
+        "description": "EXCEPT on the partition column num with 
is_colocated_by_set_op_keys hint forces a pre-partitioned (direct) exchange; 
tbl1 and tbl2 are both partitioned by num so results stay correct",
+        "sql": "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='true') */ 
{tbl1}.num FROM {tbl1} EXCEPT SELECT {tbl2}.num FROM {tbl2}"
+      },
+      {
+        "description": "UNION ALL on the partition column num with 
is_colocated_by_set_op_keys hint forces a pre-partitioned (direct) exchange; 
UNION ALL only concatenates so a direct exchange is always correct",
+        "sql": "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='true') */ 
{tbl1}.num FROM {tbl1} UNION ALL SELECT {tbl2}.num FROM {tbl2}"
+      },
+      {
+        "description": "Multi-column INTERSECT with 
is_colocated_by_set_op_keys hint; both tables are partitioned by num, a subset 
of the projected columns, so equal rows still colocate and results stay 
correct. V1 only: the V2 optimizer derives set-op distributions itself and does 
not yet handle multi-column set ops over subset-partitioned inputs",
+        "sql": "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='true') */ 
{tbl1}.num, {tbl1}.name FROM {tbl1} INTERSECT SELECT {tbl2}.num, {tbl2}.val 
FROM {tbl2}",
+        "ignoreV2Optimizer": true
+      },
+      {
+        "description": "INTERSECT with is_colocated_by_set_op_keys='false' 
disables auto-detected pre-partitioning and falls back to a shuffle; results 
stay correct",
+        "sql": "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='false') 
*/ {tbl1}.num FROM {tbl1} /*+ tableOptions(partition_function='hashcode', 
partition_key='num', partition_size='4') */ INTERSECT SELECT {tbl2}.num FROM 
{tbl2} /*+ tableOptions(partition_function='hashcode', partition_key='num', 
partition_size='4') */"
       }
     ]
   },
@@ -239,6 +260,10 @@
       {
         "description": "Colocated, Dynamic broadcast SEMI-JOIN with partially 
empty right table result for some servers",
         "sql": "SELECT /*+ joinOptions(join_strategy='dynamic_broadcast') */ 
{tbl1}.name, COUNT(*) FROM {tbl1} /*+ 
tableOptions(partition_function='hashcode', partition_key='num', 
partition_size='2') */ WHERE {tbl1}.num IN (SELECT {tbl2}.num FROM {tbl2} /*+ 
tableOptions(partition_function='hashcode', partition_key='num', 
partition_size='4') */ WHERE {tbl2}.val = 'z') GROUP BY {tbl1}.name"
+      },
+      {
+        "description": "Forcing is_colocated_by_set_op_keys='true' when the 
inputs have mismatched partition counts (tbl1=2, tbl2=4): the planner cannot 
form a direct exchange, so it still shuffles and results stay correct",
+        "sql": "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='true') */ 
{tbl1}.num FROM {tbl1} /*+ tableOptions(partition_function='hashcode', 
partition_key='num', partition_size='2') */ INTERSECT SELECT {tbl2}.num FROM 
{tbl2} /*+ tableOptions(partition_function='hashcode', partition_key='num', 
partition_size='4') */"
       }
     ]
   },


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

Reply via email to