Skip to content

Commit 3c8caaf

Browse files
xiangguangyxgclaude
andcommitted
[Enhancement] Pre-split base and rollup for INSERT-from-table into a range table
Sample-Based Tablet Pre-Split previously skipped INSERT ... SELECT ... FROM a single OLAP table when the range-distribution target carried a rollup, because the sampler could only project the base sort key and only by target column name. This makes INSERT-from-table split every visible index (base plus each rollup): - InsertFromTableScanContext carries the full target->source column-name map instead of a base-only source-column list, and the data-tier executor maps whatever sort key it is handed (request.getSortKey() for the base; each rollup spec's sort key for the secondary projection) through that map. - The shared secondary-index projection becomes an overridable executor seam whose default projects by target column name (FILES / Broker byte-identical); INSERT-from-table overrides it to remap to source names. - TablePreSplitSource builds one secondary index spec per visible rollup. - The two guards that skipped multi-index INSERT-from-table are removed. Single-index INSERT-from-table is byte-identical (the base sort key maps to the same source names). FILES / Broker and the ALTER-side pre-split path are untouched. Any unmapped sort-key column fails the sample so the load proceeds without pre-split. Data tier only (an OLAP source has no Parquet/ORC footer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: xiangguangyxg <xiangguangyxg@gmail.com>
1 parent 4f9d892 commit 3c8caaf

10 files changed

Lines changed: 353 additions & 115 deletions

fe/fe-core/src/main/java/com/starrocks/alter/reshard/presplit/AbstractSqlSampleSubqueryExecutor.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
*
4646
* <p>Subclasses implement {@link #resolveSampleSpec} to supply the FROM clause
4747
* SQL, an optional WHERE predicate, the total input byte count, the compute
48-
* resource, and the projected column identifier lists. Everything else —
48+
* resource, and the base sort-key / partition projection identifier lists; a
49+
* subclass may also override {@link #secondaryIndexProjectionIdents} to control
50+
* how each secondary index's sort key is projected. Everything else —
4951
* sampling-rate math, SQL synthesis, BE invocation, JSON row decode — is shared
5052
* here.
5153
*/
@@ -168,7 +170,7 @@ public final SampleExecution execute(SampleRequest request) throws StarRocksExce
168170
List<SecondaryIndexSpec> secondaryIndexSortKeys = request.getSecondaryIndexSortKeys();
169171
String sampleSql = buildSampleSql(
170172
spec.fromClauseSql(), spec.whereClauseSqlOrNull(),
171-
spec.sortKeyProjectionIdents(), secondaryProjectionIdents(secondaryIndexSortKeys),
173+
spec.sortKeyProjectionIdents(), secondaryProjectionIdents(request),
172174
spec.partitionProjectionIdents(),
173175
samplingRate, rowLimit, request.getSeed());
174176
List<TResultBatch> resultBatches = runSampleQuery(
@@ -180,20 +182,33 @@ public final SampleExecution execute(SampleRequest request) throws StarRocksExce
180182
}
181183

182184
/**
183-
* Flattens each {@link SecondaryIndexSpec}'s sort-key column names into
184-
* backtick-quoted SQL identifiers, in spec order then per-spec column
185-
* order. Empty when {@code secondaryIndexSortKeys} is empty, so the
186-
* projection is byte-identical to the pre-multi-index SQL.
185+
* Flattens each {@link SecondaryIndexSpec}'s projection idents, in spec order
186+
* then per-spec column order. Empty when the request carries no secondary
187+
* indexes, so the projection is byte-identical to the pre-multi-index SQL.
187188
*/
188-
private static List<String> secondaryProjectionIdents(List<SecondaryIndexSpec> secondaryIndexSortKeys) {
189+
private List<String> secondaryProjectionIdents(SampleRequest request) throws StarRocksException {
190+
List<SecondaryIndexSpec> secondaryIndexSortKeys = request.getSecondaryIndexSortKeys();
189191
if (secondaryIndexSortKeys.isEmpty()) {
190192
return List.of();
191193
}
192194
List<String> idents = new ArrayList<>();
193195
for (SecondaryIndexSpec secondaryIndexSpec : secondaryIndexSortKeys) {
194-
for (Column column : secondaryIndexSpec.sortKey()) {
195-
idents.add(SqlUtils.getIdentSql(column.getName()));
196-
}
196+
idents.addAll(secondaryIndexProjectionIdents(request, secondaryIndexSpec));
197+
}
198+
return idents;
199+
}
200+
201+
/**
202+
* Projection idents to SELECT for one secondary index's sort key. The default
203+
* projects each column by its own name, correct when the source is name-aligned
204+
* to the target. A subclass that remaps which source columns back a target column
205+
* overrides this to project by the source-table column name instead.
206+
*/
207+
protected List<String> secondaryIndexProjectionIdents(SampleRequest request, SecondaryIndexSpec spec)
208+
throws StarRocksException {
209+
List<String> idents = new ArrayList<>(spec.sortKey().size());
210+
for (Column column : spec.sortKey()) {
211+
idents.add(SqlUtils.getIdentSql(column.getName()));
197212
}
198213
return idents;
199214
}

fe/fe-core/src/main/java/com/starrocks/alter/reshard/presplit/InsertFromTableSampleSubqueryExecutor.java

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
package com.starrocks.alter.reshard.presplit;
1616

1717
import com.google.common.annotations.VisibleForTesting;
18+
import com.starrocks.catalog.Column;
1819
import com.starrocks.common.StarRocksException;
1920

21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Map;
24+
2025
/**
2126
* Data-tier {@link SampleSubqueryExecutor} for the INSERT-from-OLAP-table path.
2227
* Synthesizes a {@code SELECT <sort_key_cols>[, <partition_source_cols>] FROM
@@ -45,20 +50,58 @@ final class InsertFromTableSampleSubqueryExecutor extends AbstractSqlSampleSubqu
4550

4651
@Override
4752
protected SampleSpec resolveSampleSpec(SampleRequest request) throws StarRocksException {
48-
ScanContext scanContext = request.getScanContext();
49-
if (!(scanContext instanceof InsertFromTableScanContext context)) {
50-
throw new StarRocksException(ERROR_PREFIX + "received a "
51-
+ scanContext.getClass().getSimpleName()
52-
+ " — wire only the INSERT-from-table load kind here");
53-
}
53+
InsertFromTableScanContext context = contextOf(request);
5454
return new SampleSpec(
5555
context.sourceFromSql(),
5656
context.wherePredicateSql(),
5757
Math.max(0L, context.sourceTable().getDataSize()),
5858
context.computeResource(),
59-
identsOf(context.sortKeySourceColumnNames()),
59+
identsOf(mapToSource(request.getSortKey(), context.targetToSourceColumnNames())),
6060
identsOf(context.partitionSourceColumnNames()),
6161
request.getSortKey(),
6262
request.getPartitionSourceColumns());
6363
}
64+
65+
/**
66+
* Projects one rollup's sort key by remapping each target column to its source-table
67+
* name. The base sort key is projected the same way in {@link #resolveSampleSpec}; both
68+
* go through {@link #mapToSource}, so a rollup whose sort key reorders or subsets the base
69+
* is projected correctly regardless of source column naming.
70+
*/
71+
@Override
72+
protected List<String> secondaryIndexProjectionIdents(SampleRequest request, SecondaryIndexSpec spec)
73+
throws StarRocksException {
74+
InsertFromTableScanContext context = contextOf(request);
75+
return identsOf(mapToSource(spec.sortKey(), context.targetToSourceColumnNames()));
76+
}
77+
78+
private static InsertFromTableScanContext contextOf(SampleRequest request) throws StarRocksException {
79+
ScanContext scanContext = request.getScanContext();
80+
if (!(scanContext instanceof InsertFromTableScanContext context)) {
81+
throw new StarRocksException(ERROR_PREFIX + "received a "
82+
+ scanContext.getClass().getSimpleName()
83+
+ " -- wire only the INSERT-from-table load kind here");
84+
}
85+
return context;
86+
}
87+
88+
/**
89+
* Remaps target sort-key columns to their source-table column names via the scan
90+
* context's target-&gt;source map. Throws (-&gt; the sample fails -&gt; the load proceeds
91+
* without pre-split) if any column is unmapped, so a boundary is never computed against
92+
* the wrong source column.
93+
*/
94+
private static List<String> mapToSource(
95+
List<Column> targetColumns, Map<String, String> targetToSourceColumnNames) throws StarRocksException {
96+
List<String> sourceNames = new ArrayList<>(targetColumns.size());
97+
for (Column column : targetColumns) {
98+
String sourceName = targetToSourceColumnNames.get(column.getName().toLowerCase());
99+
if (sourceName == null) {
100+
throw new StarRocksException(ERROR_PREFIX
101+
+ "no source column mapped for target sort-key column " + column.getName());
102+
}
103+
sourceNames.add(sourceName);
104+
}
105+
return sourceNames;
106+
}
64107
}

fe/fe-core/src/main/java/com/starrocks/alter/reshard/presplit/InsertFromTableScanContext.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,31 @@
1818
import com.starrocks.warehouse.cngroup.ComputeResource;
1919

2020
import java.util.List;
21+
import java.util.Map;
2122
import java.util.Objects;
2223

2324
/**
2425
* {@link ScanContext} concrete for the INSERT-from-OLAP-table integration.
2526
* Carries the source {@link OlapTable} reference so the sampler can obtain its
26-
* data-size estimate and the pre-quoted FROM clause SQL, plus the source-column
27-
* name lists that map the target sort key and partition columns back to their
28-
* source equivalents. The optional WHERE predicate SQL is threaded through
29-
* verbatim from the INSERT-SELECT statement so the sample covers only the rows
30-
* the load will actually write.
27+
* data-size estimate and the pre-quoted FROM clause SQL, the full target-&gt;source
28+
* column-name map the sampler uses to project any index's sort key (base or rollup)
29+
* by its source-table column name, and the source-column names of the target
30+
* partition columns. The optional WHERE predicate SQL is threaded through verbatim
31+
* from the INSERT-SELECT statement so the sample covers only the rows the load will
32+
* actually write.
3133
*/
3234
public record InsertFromTableScanContext(
3335
OlapTable sourceTable,
3436
String sourceFromSql, // "`db`.`tbl` `alias`" or "`db`.`tbl`"
35-
List<String> sortKeySourceColumnNames,
37+
Map<String, String> targetToSourceColumnNames, // lower-cased target name -> source column name
3638
List<String> partitionSourceColumnNames,
3739
String wherePredicateSql, // nullable
3840
ComputeResource computeResource) implements ScanContext {
3941

4042
public InsertFromTableScanContext {
4143
Objects.requireNonNull(sourceTable, "sourceTable");
4244
Objects.requireNonNull(sourceFromSql, "sourceFromSql");
43-
Objects.requireNonNull(sortKeySourceColumnNames, "sortKeySourceColumnNames");
45+
Objects.requireNonNull(targetToSourceColumnNames, "targetToSourceColumnNames");
4446
Objects.requireNonNull(partitionSourceColumnNames, "partitionSourceColumnNames");
4547
Objects.requireNonNull(computeResource, "computeResource");
4648
}

fe/fe-core/src/main/java/com/starrocks/alter/reshard/presplit/InsertSelectSourceColumns.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,28 @@
3737
* (caller then silently skips pre-split).
3838
*/
3939
final class InsertSelectSourceColumns {
40-
private final List<String> sortKeySourceColumnNames;
4140
private final List<String> partitionSourceColumnNames;
41+
private final Map<String, String> targetToSourceColumnNames;
4242

43-
private InsertSelectSourceColumns(List<String> sortKeySourceColumnNames,
44-
List<String> partitionSourceColumnNames) {
45-
this.sortKeySourceColumnNames = sortKeySourceColumnNames;
43+
private InsertSelectSourceColumns(List<String> partitionSourceColumnNames,
44+
Map<String, String> targetToSourceColumnNames) {
4645
this.partitionSourceColumnNames = partitionSourceColumnNames;
47-
}
48-
49-
List<String> sortKeySourceColumnNames() {
50-
return sortKeySourceColumnNames;
46+
this.targetToSourceColumnNames = targetToSourceColumnNames;
5147
}
5248

5349
List<String> partitionSourceColumnNames() {
5450
return partitionSourceColumnNames;
5551
}
5652

53+
/**
54+
* Full target-&gt;source column-name map (lower-cased target name -&gt; source column name),
55+
* total over the target's non-generated base columns. The sampler uses it to project any
56+
* index's sort key (base or rollup) by its source-table column name.
57+
*/
58+
Map<String, String> targetToSourceColumnNames() {
59+
return targetToSourceColumnNames;
60+
}
61+
5762
/**
5863
* Resolves the source-column names for each sort-key and partition column of the target table.
5964
*
@@ -148,15 +153,15 @@ static InsertSelectSourceColumns resolve(
148153
}
149154
}
150155

151-
List<String> sortKeyNames = lookup(sortKeyColumns, targetToSource);
152-
if (sortKeyNames == null) {
156+
// A sort-key column with no source mapping cannot be sampled -> skip pre-split.
157+
if (lookup(sortKeyColumns, targetToSource) == null) {
153158
return null;
154159
}
155160
List<String> partitionNames = lookup(partitionColumns, targetToSource);
156161
if (partitionNames == null) {
157162
return null;
158163
}
159-
return new InsertSelectSourceColumns(sortKeyNames, partitionNames);
164+
return new InsertSelectSourceColumns(partitionNames, Map.copyOf(targetToSource));
160165
}
161166

162167
private static Set<String> targetNames(List<Column> targetCols) {

fe/fe-core/src/main/java/com/starrocks/alter/reshard/presplit/PreSplitFlow.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@
4242
* <li>{@link #dispatch} — partitioned-vs-unpartitioned routing, including the
4343
* automatic-partition gate that conservatively skips manually
4444
* list/range-partitioned targets (those cannot pre-create partitions from
45-
* sampled values), and the INSERT-from-table rollup descope (a rollup's sort
46-
* key cannot be remapped to source columns, so a multi-index target skips
47-
* before sampling for that load kind only).</li>
45+
* sampled values).</li>
4846
* <li>{@link #runSinglePartitionFlow} — resolve the unique partition + base
4947
* tablet, build a {@link DefaultPreSplitPipeline}, submit via
5048
* {@link TabletPreSplitCoordinator#submitAsynchronously}, then sync-await
@@ -76,7 +74,7 @@ private PreSplitFlow() {
7674
* requested tablet count; computeResource sizes the active CN count; scanContext carries
7775
* the source-specific scan inputs; secondaryIndexSpecs names every OTHER visible index
7876
* (rollup) whose sort key the multi-partition data-tier sampler should project alongside
79-
* the base sort key -- empty for single-index targets and for INSERT-from-table (descoped).
77+
* the base sort key -- empty for single-index targets.
8078
*/
8179
record Prepared(ScanContext scanContext, List<Column> sortKeyColumns,
8280
List<Column> partitionColumns, long estimatedBytes,
@@ -94,11 +92,6 @@ record Prepared(ScanContext scanContext, List<Column> sortKeyColumns,
9492

9593
static void dispatch(Database database, OlapTable target, Prepared prepared,
9694
LoadKind loadKind, BooleanSupplier shouldAbort, ConnectContext context) {
97-
if (loadKind == LoadKind.INSERT_FROM_TABLE && target.getVisibleIndexMetas().size() > 1) {
98-
// INSERT-from-table cannot remap a divergent rollup sort key to source columns (descoped).
99-
PreSplitMetrics.recordEligibilitySkip(SkipReason.HAS_MATERIALIZED_VIEW_OR_ROLLUP);
100-
return;
101-
}
10295
if (target.getPartitionInfo().isPartitioned()) {
10396
// Manually list/range-partitioned targets do not support pre-creating partitions
10497
// from sampled values; skip conservatively, let the load proceed.
@@ -117,13 +110,6 @@ static void runSinglePartitionFlow(Database database, OlapTable table, Prepared
117110
if (target == null) {
118111
return;
119112
}
120-
if (loadKind == LoadKind.INSERT_FROM_TABLE && target.indexTargets().size() > 1) {
121-
// Authoritative re-check on the resolved index set: a rollup that became visible after the
122-
// dispatch-time descope gate must not be sampled with the base-only INSERT-from-table source
123-
// mapping. Skip pre-split (load proceeds) -- INSERT-from-table with a rollup is out of scope.
124-
PreSplitMetrics.recordEligibilitySkip(SkipReason.HAS_MATERIALIZED_VIEW_OR_ROLLUP);
125-
return;
126-
}
127113
int activeComputeNodeCount = TabletReshardUtils.computeNodeCount(prepared.computeResource());
128114
DefaultPreSplitPipeline pipeline = DefaultPreSplitPipeline.forLoadKind(
129115
target.database(), target.olapTable(), target.indexTargets(), prepared.estimatedBytes(), loadKind,

fe/fe-core/src/main/java/com/starrocks/alter/reshard/presplit/TablePreSplitSource.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.starrocks.authorization.PrivilegeType;
1919
import com.starrocks.catalog.Column;
2020
import com.starrocks.catalog.Database;
21+
import com.starrocks.catalog.MaterializedIndexMeta;
2122
import com.starrocks.catalog.OlapTable;
2223
import com.starrocks.catalog.Table;
2324
import com.starrocks.catalog.TableName;
@@ -37,6 +38,7 @@
3738
import com.starrocks.sql.ast.expression.SlotRef;
3839
import com.starrocks.sql.common.MetaUtils;
3940

41+
import java.util.ArrayList;
4042
import java.util.List;
4143
import java.util.Map;
4244

@@ -109,14 +111,30 @@ public PreSplitFlow.Prepared prepare(InsertStmt insertStmt, SelectRelation selec
109111
}
110112
InsertFromTableScanContext scanContext = new InsertFromTableScanContext(
111113
resolvedSource.sourceTable(), resolvedSource.sourceFromSql(),
112-
mapping.sortKeySourceColumnNames(), mapping.partitionSourceColumnNames(),
114+
mapping.targetToSourceColumnNames(), mapping.partitionSourceColumnNames(),
113115
wherePredicateSql, context.getCurrentComputeResource());
114116
long estimatedBytes = Math.max(0L, resolvedSource.sourceTable().getDataSize());
115-
// INSERT-from-table cannot remap a rollup's sort key to source columns, so the
116-
// dispatch gate already skips multi-index targets for this load kind; secondaryIndexSpecs
117-
// stays empty here via the backward-compatible Prepared constructor.
118117
return new PreSplitFlow.Prepared(scanContext, sortKeyColumns, partitionColumns,
119-
estimatedBytes, context.getCurrentComputeResource());
118+
estimatedBytes, context.getCurrentComputeResource(), buildSecondaryIndexSpecs(target));
119+
}
120+
121+
/**
122+
* Builds one {@link SecondaryIndexSpec} per visible rollup index of {@code target}
123+
* (every visible index except the base), each carrying its own TARGET sort-key columns.
124+
* The data-tier executor remaps every column to its source-table name via the scan
125+
* context's target-&gt;source map at sample time, so the specs carry TARGET columns here --
126+
* identical in shape to the FILES / Broker builders.
127+
*/
128+
private static List<SecondaryIndexSpec> buildSecondaryIndexSpecs(OlapTable target) {
129+
List<SecondaryIndexSpec> specs = new ArrayList<>();
130+
for (MaterializedIndexMeta meta : target.getVisibleIndexMetas()) {
131+
if (meta.getIndexMetaId() == target.getBaseIndexMetaId()) {
132+
continue;
133+
}
134+
specs.add(new SecondaryIndexSpec(meta.getIndexMetaId(),
135+
MetaUtils.getRangeDistributionColumns(target, meta.getIndexMetaId())));
136+
}
137+
return specs;
120138
}
121139

122140
/**

0 commit comments

Comments
 (0)