Skip to content

Commit bff5d4e

Browse files
Hyper-FFmergify[bot]
authored andcommitted
[BugFix] Name a partition column's key range by column id, not by column name (#77988)
(cherry picked from commit e843b3c) # Conflicts: # fe/fe-core/src/main/java/com/starrocks/sql/plan/PlanFragmentBuilder.java
1 parent 086e15c commit bff5d4e

2 files changed

Lines changed: 282 additions & 0 deletions

File tree

fe/fe-core/src/main/java/com/starrocks/sql/plan/PlanFragmentBuilder.java

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,193 @@ public PlanFragment visitPhysicalOlapScan(OptExpression optExpr, ExecPlan contex
10861086
return fragment;
10871087
}
10881088

1089+
<<<<<<< HEAD
1090+
=======
1091+
/**
1092+
* Compute partition key ranges for dynamic partition pruning.
1093+
* Returns a list of TKeyRange describing the value ranges of each used partition column.
1094+
* Returns empty if the partition has too many values (exceeding the session limit) or
1095+
* if the partition type is unsupported.
1096+
*/
1097+
private List<TKeyRange> computePartitionRange(OlapTable table, Partition partition,
1098+
Collection<Column> usedPartitionCols, SessionVariable session) {
1099+
PartitionInfo partitionInfo = table.getPartitionInfo();
1100+
if (usedPartitionCols.isEmpty() || !partition.hasData()
1101+
|| !(partitionInfo.isRangePartition() || partitionInfo.isListPartition())) {
1102+
return List.of();
1103+
}
1104+
List<Column> partitionCols = partitionInfo.getPartitionColumns(table.getIdToColumn());
1105+
Preconditions.checkState(partitionCols.containsAll(usedPartitionCols));
1106+
1107+
long limit = session.getDynamicPartitionPruneValuesLimit();
1108+
if (partitionInfo.isRangePartition()) {
1109+
return computeRangePartitionKeyRanges(partitionInfo, partition, partitionCols, usedPartitionCols, limit);
1110+
} else {
1111+
return computeListPartitionKeyRanges(partitionInfo, partition, partitionCols, usedPartitionCols, limit);
1112+
}
1113+
}
1114+
1115+
private List<TKeyRange> computeRangePartitionKeyRanges(PartitionInfo partitionInfo, Partition partition,
1116+
List<Column> partitionCols,
1117+
Collection<Column> usedPartitionCols, long limit) {
1118+
RangePartitionInfo rangeInfo = (RangePartitionInfo) partitionInfo;
1119+
Range<PartitionKey> keyRange = rangeInfo.getRange(partition.getId());
1120+
if (!keyRange.hasLowerBound() || !keyRange.hasUpperBound()) {
1121+
return List.of();
1122+
}
1123+
1124+
boolean isNullPartition = keyRange.lowerEndpoint().isMinValue();
1125+
long partitionValues = 1;
1126+
List<TKeyRange> result = Lists.newArrayList();
1127+
1128+
for (int i = 0; i < partitionCols.size(); i++) {
1129+
Column col = partitionCols.get(i);
1130+
if (!usedPartitionCols.contains(col)) {
1131+
continue;
1132+
}
1133+
1134+
TKeyRange kr = new TKeyRange();
1135+
long rangeSize;
1136+
1137+
if (col.getType().isDate()) {
1138+
LiteralExpr lowerExpr = keyRange.lowerEndpoint().getKeys().get(i);
1139+
LiteralExpr upperExpr = keyRange.upperEndpoint().getKeys().get(i);
1140+
if (!(lowerExpr instanceof DateLiteral lower) || !(upperExpr instanceof DateLiteral upper)) {
1141+
continue;
1142+
}
1143+
kr.setBegin_key(lower.getYear() * 10000 + lower.getMonth() * 100 + lower.getDay());
1144+
kr.setEnd_key(upper.getYear() * 10000 + upper.getMonth() * 100 + upper.getDay());
1145+
rangeSize = upper.toLocalDateTime().toLocalDate().toEpochDay()
1146+
- lower.toLocalDateTime().toLocalDate().toEpochDay();
1147+
} else if (col.getType().isIntegerType()) {
1148+
long lowerVal = keyRange.lowerEndpoint().getKeys().get(i).getLongValue();
1149+
long upperVal = keyRange.upperEndpoint().getKeys().get(i).getLongValue();
1150+
kr.setBegin_key(lowerVal);
1151+
kr.setEnd_key(upperVal);
1152+
rangeSize = upperVal - lowerVal;
1153+
} else {
1154+
continue;
1155+
}
1156+
1157+
if (rangeSize <= 0 || wouldOverflowOrExceedLimit(partitionValues, rangeSize, limit)) {
1158+
break;
1159+
}
1160+
partitionValues *= rangeSize;
1161+
1162+
kr.setColumn_type(TypeSerializer.toThrift(col.getType().getPrimitiveType()));
1163+
// BE indexes the tuple's slots by col_name, which is the column id, so name the range by
1164+
// the id as well: a renamed partition column would otherwise be skipped and its
1165+
// scan ranges never pruned.
1166+
kr.setColumn_name(col.getColumnId().getId());
1167+
if (isNullPartition) {
1168+
kr.setHas_null(true);
1169+
}
1170+
result.add(kr);
1171+
}
1172+
1173+
return result;
1174+
}
1175+
1176+
private List<TKeyRange> computeListPartitionKeyRanges(PartitionInfo partitionInfo, Partition partition,
1177+
List<Column> partitionCols,
1178+
Collection<Column> usedPartitionCols, long limit) {
1179+
ListPartitionInfo listInfo = (ListPartitionInfo) partitionInfo;
1180+
if (listInfo.getLiteralExprValues().containsKey(partition.getId())) {
1181+
return computeSingleColumnListKeyRanges(listInfo, partition, partitionCols, usedPartitionCols, limit);
1182+
} else if (listInfo.getMultiLiteralExprValues().containsKey(partition.getId())) {
1183+
return computeMultiColumnListKeyRanges(listInfo, partition, partitionCols, usedPartitionCols, limit);
1184+
} else {
1185+
return List.of();
1186+
}
1187+
}
1188+
1189+
private List<TKeyRange> computeSingleColumnListKeyRanges(ListPartitionInfo listInfo, Partition partition,
1190+
List<Column> partitionCols,
1191+
Collection<Column> usedPartitionCols, long limit) {
1192+
Preconditions.checkState(partitionCols.size() == 1);
1193+
List<LiteralExpr> partitionValuesList = listInfo.getLiteralExprValues().get(partition.getId());
1194+
long partitionValues = 1;
1195+
List<TKeyRange> result = Lists.newArrayList();
1196+
1197+
for (Column col : partitionCols) {
1198+
if (!usedPartitionCols.contains(col)) {
1199+
continue;
1200+
}
1201+
1202+
long listSize = partitionValuesList.size();
1203+
if (wouldOverflowOrExceedLimit(partitionValues, listSize, limit)) {
1204+
break;
1205+
}
1206+
partitionValues *= listSize;
1207+
1208+
TKeyRange kr = new TKeyRange();
1209+
kr.setColumn_type(TypeSerializer.toThrift(col.getType().getPrimitiveType()));
1210+
// BE indexes the tuple's slots by col_name, which is the column id, so name the range by
1211+
// the id as well: a renamed partition column would otherwise be skipped and its
1212+
// scan ranges never pruned.
1213+
kr.setColumn_name(col.getColumnId().getId());
1214+
List<TExpr> l = Lists.newArrayList();
1215+
partitionValuesList.forEach(v -> l.add(ExprToThrift.treeToThrift(v)));
1216+
kr.setList_values(l);
1217+
result.add(kr);
1218+
}
1219+
1220+
return result;
1221+
}
1222+
1223+
private List<TKeyRange> computeMultiColumnListKeyRanges(ListPartitionInfo listInfo, Partition partition,
1224+
List<Column> partitionCols,
1225+
Collection<Column> usedPartitionCols, long limit) {
1226+
List<List<LiteralExpr>> partitionValuesList = listInfo.getMultiLiteralExprValues().get(partition.getId());
1227+
long partitionValues = 1;
1228+
List<TKeyRange> result = Lists.newArrayList();
1229+
1230+
for (int i = 0; i < partitionCols.size(); i++) {
1231+
Column col = partitionCols.get(i);
1232+
if (!usedPartitionCols.contains(col)) {
1233+
continue;
1234+
}
1235+
1236+
long listSize = partitionValuesList.size();
1237+
if (wouldOverflowOrExceedLimit(partitionValues, listSize, limit)) {
1238+
break;
1239+
}
1240+
partitionValues *= listSize;
1241+
1242+
TKeyRange kr = new TKeyRange();
1243+
kr.setColumn_type(TypeSerializer.toThrift(col.getType().getPrimitiveType()));
1244+
// BE indexes the tuple's slots by col_name, which is the column id, so name the range by
1245+
// the id as well: a renamed partition column would otherwise be skipped and its
1246+
// scan ranges never pruned.
1247+
kr.setColumn_name(col.getColumnId().getId());
1248+
List<TExpr> l = Lists.newArrayList();
1249+
for (var values : partitionValuesList) {
1250+
Preconditions.checkState(values.size() == partitionCols.size());
1251+
l.add(ExprToThrift.treeToThrift(values.get(i)));
1252+
}
1253+
kr.setList_values(l);
1254+
result.add(kr);
1255+
}
1256+
1257+
return result;
1258+
}
1259+
1260+
/**
1261+
* Check if multiplying {@code current} by {@code factor} would overflow {@code long}
1262+
* or if the product would exceed the given {@code limit}.
1263+
*/
1264+
private static boolean wouldOverflowOrExceedLimit(long current, long factor, long limit) {
1265+
if (factor == 0) {
1266+
return false;
1267+
}
1268+
// Overflow check: current * factor > Long.MAX_VALUE
1269+
if (current > limit / factor) {
1270+
return true;
1271+
}
1272+
return current * factor > limit;
1273+
}
1274+
1275+
>>>>>>> e843b3cab2 ([BugFix] Name a partition column's key range by column id, not by column name (#77988))
10891276
@NotNull
10901277
private static Map<Integer, Expr> getGlobalDictsExprs(Map<Integer, ScalarOperator> dictExprs,
10911278
ExecPlan context) {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2021-present StarRocks, Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.starrocks.planner;
16+
17+
import com.google.common.collect.Lists;
18+
import com.starrocks.common.FeConstants;
19+
import com.starrocks.common.Pair;
20+
import com.starrocks.common.util.UUIDUtil;
21+
import com.starrocks.qe.ConnectContext;
22+
import com.starrocks.sql.plan.ExecPlan;
23+
import com.starrocks.thrift.TInternalScanRange;
24+
import com.starrocks.thrift.TKeyRange;
25+
import com.starrocks.thrift.TScanRangeLocations;
26+
import com.starrocks.utframe.StarRocksAssert;
27+
import com.starrocks.utframe.UtFrameUtils;
28+
import org.junit.jupiter.api.Assertions;
29+
import org.junit.jupiter.api.BeforeAll;
30+
import org.junit.jupiter.api.Test;
31+
32+
import java.util.List;
33+
34+
/**
35+
* A scan range carries the value range of each partition column it was cut by, and BE's dynamic
36+
* partition pruning evaluates the partition conjuncts against those values to drop scan ranges the
37+
* predicate cannot match. It finds the column those values belong to in a map keyed by the slot's
38+
* col_name - the storage-side column id - so the range has to name the column by its id as well.
39+
*/
40+
public class PartitionKeyRangeColumnIdTest {
41+
private static ConnectContext connectContext;
42+
private static StarRocksAssert starRocksAssert;
43+
44+
@BeforeAll
45+
public static void setUp() throws Exception {
46+
FeConstants.runningUnitTest = true;
47+
UtFrameUtils.createMinStarRocksCluster();
48+
connectContext = UtFrameUtils.createDefaultCtx();
49+
connectContext.setQueryId(UUIDUtil.genUUID());
50+
starRocksAssert = new StarRocksAssert(connectContext);
51+
starRocksAssert.withDatabase("test_pkr").useDatabase("test_pkr")
52+
.withTable("create table pkr(dt date not null, v int) duplicate key(dt)" +
53+
" partition by range(dt) (" +
54+
" partition p1 values [('2026-01-01'), ('2026-01-02'))," +
55+
" partition p2 values [('2026-01-02'), ('2026-01-03')))" +
56+
" distributed by hash(v) buckets 1 properties('replication_num' = '1');");
57+
}
58+
59+
@Test
60+
public void testPartitionKeyRangeNamesTheColumnById() throws Exception {
61+
// dayofmonth() is not something FE can prune partitions by, so both partitions survive into
62+
// the plan and BE gets a chance to drop their scan ranges - the case this naming decides.
63+
String sql = "select v from test_pkr.pkr where dayofmonth(%s) = 1";
64+
Assertions.assertEquals(List.of("dt", "dt"), rangeColumnNames(String.format(sql, "dt")));
65+
66+
starRocksAssert.ddl("alter table pkr rename column dt to dt_new");
67+
try {
68+
Assertions.assertEquals(List.of("dt", "dt"), rangeColumnNames(String.format(sql, "dt_new")),
69+
"a partition column range must name its column by the id, which the rename left "
70+
+ "alone, or BE's column_name_to_slot lookup misses it and prunes nothing");
71+
} finally {
72+
starRocksAssert.ddl("alter table pkr rename column dt_new to dt");
73+
}
74+
}
75+
76+
private static List<String> rangeColumnNames(String sql) throws Exception {
77+
connectContext.setQueryId(UUIDUtil.genUUID());
78+
connectContext.setExecutionId(UUIDUtil.toTUniqueId(connectContext.getQueryId()));
79+
Pair<String, ExecPlan> plan = UtFrameUtils.getPlanAndFragment(connectContext, sql);
80+
List<String> names = Lists.newArrayList();
81+
for (ScanNode scanNode : plan.second.getScanNodes()) {
82+
for (TScanRangeLocations locations : scanNode.getScanRangeLocations(0)) {
83+
TInternalScanRange range = locations.getScan_range().getInternal_scan_range();
84+
if (range == null || range.getPartition_column_ranges() == null) {
85+
continue;
86+
}
87+
for (TKeyRange keyRange : range.getPartition_column_ranges()) {
88+
names.add(keyRange.getColumn_name());
89+
}
90+
}
91+
}
92+
Assertions.assertFalse(names.isEmpty(), "the plan must carry partition column ranges at all");
93+
return names;
94+
}
95+
}

0 commit comments

Comments
 (0)