Skip to content

Commit f216ed2

Browse files
authored
Add tests for chaining colocated joins, window functions and set operations without shuffle (#18997)
1 parent 944ef22 commit f216ed2

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,84 @@ private List<MailboxSendNode> findSetOpInputSendNodes(DispatchableSubPlan dispat
11611161
return sendNodes;
11621162
}
11631163

1164+
/// When colocation hints are applied to a chain of operations that are all keyed on the same (partition) column, the
1165+
/// whole chain executes without a data shuffle: every hash-distributed exchange in the plan is pre-partitioned. This
1166+
/// covers combinations of colocated joins, window functions and set operations (see {@link #colocatedChains}). The
1167+
/// join key is {@code a.col2}/{@code b.col1} (each table's partition column), so the join is colocated, and the
1168+
/// window and set op keep that same key.
1169+
@Test(dataProvider = "colocatedChains")
1170+
public void testColocatedOperationsChainWithoutShuffle(String description, String query) {
1171+
List<MailboxSendNode> hashSends = findHashSendNodes(_queryEnvironment.planQuery(query));
1172+
assertFalse(hashSends.isEmpty(), "Expected at least one hash exchange in the chain: " + description);
1173+
for (MailboxSendNode sendNode : hashSends) {
1174+
assertTrue(sendNode.isPrePartitioned(),
1175+
"Colocated chain '" + description + "' should have no shuffle, but a hash exchange was not pre-partitioned");
1176+
}
1177+
}
1178+
1179+
@DataProvider(name = "colocatedChains")
1180+
private Object[][] colocatedChains() {
1181+
// joinOptions + windowOptions together (attach to the join and window in a single SELECT).
1182+
String joinAndWindowHint = "/*+ joinOptions(is_colocated_by_join_keys='true'), "
1183+
+ "windowOptions(is_partitioned_by_window_keys='true') */";
1184+
String joinHint = "/*+ joinOptions(is_colocated_by_join_keys='true') */";
1185+
String windowHint = "/*+ windowOptions(is_partitioned_by_window_keys='true') */";
1186+
String setOpHint = "/*+ setOpOptions(is_colocated_by_set_op_keys='true') */";
1187+
String join = "a JOIN b ON a.col2 = b.col1";
1188+
return new Object[][]{
1189+
{"join -> window",
1190+
"SELECT " + joinAndWindowHint + " a.col2, SUM(a.col3) OVER (PARTITION BY a.col2) FROM " + join},
1191+
{"set op of colocated joins", "SELECT " + setOpHint + " * FROM (SELECT " + joinHint + " a.col2 FROM " + join
1192+
+ " UNION ALL SELECT " + joinHint + " a.col2 FROM " + join + ")"},
1193+
{"set op of colocated windows", "SELECT " + setOpHint + " * FROM (SELECT " + windowHint
1194+
+ " col2, SUM(col3) OVER (PARTITION BY col2) FROM a UNION ALL SELECT " + windowHint
1195+
+ " col2, SUM(col3) OVER (PARTITION BY col2) FROM a)"},
1196+
{"join -> window -> set op", "SELECT " + setOpHint + " * FROM (SELECT " + joinAndWindowHint
1197+
+ " a.col2, SUM(a.col3) OVER (PARTITION BY a.col2) FROM " + join + " UNION ALL SELECT " + joinAndWindowHint
1198+
+ " a.col2, SUM(a.col3) OVER (PARTITION BY a.col2) FROM " + join + ")"},
1199+
};
1200+
}
1201+
1202+
/// Baseline for {@link #testColocatedOperationsChainWithoutShuffle}: without the colocation hints the same
1203+
/// join -> window chain still shuffles (at least one hash exchange is not pre-partitioned), proving the hints are
1204+
/// what eliminate the shuffles.
1205+
@Test
1206+
public void testChainWithoutColocationHintsStillShuffles() {
1207+
String query = "SELECT a.col2, SUM(a.col3) OVER (PARTITION BY a.col2) FROM a JOIN b ON a.col2 = b.col1";
1208+
List<MailboxSendNode> hashSends = findHashSendNodes(_queryEnvironment.planQuery(query));
1209+
assertTrue(hashSends.stream().anyMatch(sendNode -> !sendNode.isPrePartitioned()),
1210+
"Without the colocation hints, the join -> window chain should contain at least one shuffled hash exchange");
1211+
}
1212+
1213+
/// Set-op-specific baseline for the "set op of colocated joins" chain: with
1214+
/// {@code is_colocated_by_set_op_keys='false'} the set-op exchange is forced back to a shuffle (while the per-branch
1215+
/// joins stay colocated), proving the set-op hint is what keeps the set-op level of the chain colocated.
1216+
@Test
1217+
public void testSetOpChainWithoutSetOpHintShuffles() {
1218+
String join = "a JOIN b ON a.col2 = b.col1";
1219+
String joinHint = "/*+ joinOptions(is_colocated_by_join_keys='true') */";
1220+
String query = "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='false') */ * FROM (SELECT " + joinHint
1221+
+ " a.col2 FROM " + join + " UNION ALL SELECT " + joinHint + " a.col2 FROM " + join + ")";
1222+
List<MailboxSendNode> hashSends = findHashSendNodes(_queryEnvironment.planQuery(query));
1223+
assertTrue(hashSends.stream().anyMatch(sendNode -> !sendNode.isPrePartitioned()),
1224+
"With is_colocated_by_set_op_keys='false', the set-op level of the chain should shuffle");
1225+
}
1226+
1227+
/// Collects the {@link MailboxSendNode} at the root of every hash-distributed stage in the plan (i.e. every
1228+
/// inter-stage hash exchange). A pre-partitioned send is a direct, no-shuffle exchange; a non-pre-partitioned one is
1229+
/// a full shuffle.
1230+
private List<MailboxSendNode> findHashSendNodes(DispatchableSubPlan dispatchableSubPlan) {
1231+
List<MailboxSendNode> sendNodes = new ArrayList<>();
1232+
for (DispatchablePlanFragment fragment : dispatchableSubPlan.getQueryStages()) {
1233+
PlanNode root = fragment.getPlanFragment().getFragmentRoot();
1234+
if (root instanceof MailboxSendNode
1235+
&& ((MailboxSendNode) root).getDistributionType() == RelDistribution.Type.HASH_DISTRIBUTED) {
1236+
sendNodes.add((MailboxSendNode) root);
1237+
}
1238+
}
1239+
return sendNodes;
1240+
}
1241+
11641242
/**
11651243
* Finds the DispatchablePlanFragment containing a JoinNode (non-leaf, non-root stage).
11661244
*/

pinot-query-runtime/src/test/resources/queries/QueryHints.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,14 @@
183183
{
184184
"description": "INTERSECT with is_colocated_by_set_op_keys='false' disables auto-detected pre-partitioning and falls back to a shuffle; results stay correct",
185185
"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') */"
186+
},
187+
{
188+
"description": "Chained colocated operations stay correct: a colocated JOIN on num feeding a window partitioned by num (the no-shuffle plan shape is asserted in QueryCompilationTest)",
189+
"sql": "SELECT /*+ joinOptions(is_colocated_by_join_keys='true'), windowOptions(is_partitioned_by_window_keys='true') */ {tbl1}.num, COUNT(*) OVER (PARTITION BY {tbl1}.num) FROM {tbl1} /*+ tableOptions(partition_function='hashcode', partition_key='num', partition_size='4') */ JOIN {tbl2} /*+ tableOptions(partition_function='hashcode', partition_key='num', partition_size='4') */ ON {tbl1}.num = {tbl2}.num"
190+
},
191+
{
192+
"description": "Chained colocated operations stay correct: a UNION ALL of two colocated JOINs on num, set-op hint on the outer wrap and join hint per branch (the no-shuffle plan shape is asserted in QueryCompilationTest)",
193+
"sql": "SELECT /*+ setOpOptions(is_colocated_by_set_op_keys='true') */ * FROM (SELECT /*+ joinOptions(is_colocated_by_join_keys='true') */ {tbl1}.num FROM {tbl1} /*+ tableOptions(partition_function='hashcode', partition_key='num', partition_size='4') */ JOIN {tbl2} /*+ tableOptions(partition_function='hashcode', partition_key='num', partition_size='4') */ ON {tbl1}.num = {tbl2}.num UNION ALL SELECT /*+ joinOptions(is_colocated_by_join_keys='true') */ {tbl1}.num FROM {tbl1} /*+ tableOptions(partition_function='hashcode', partition_key='num', partition_size='4') */ JOIN {tbl2} /*+ tableOptions(partition_function='hashcode', partition_key='num', partition_size='4') */ ON {tbl1}.num = {tbl2}.num)"
186194
}
187195
]
188196
},

0 commit comments

Comments
 (0)