foxtail463 commented on code in PR #68648:
URL: https://github.com/apache/doris/pull/68648#discussion_r4305780645
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java:
##########
@@ -130,17 +142,98 @@ private static List<String>
constructPartsForMv(Set<String> partitionNames) {
return Lists.newArrayList(partitionNames);
}
+ /**
+ * The predicate every base table of the MV definition is read through.
+ *
+ * <p>A table the caller scopes is read from exactly the base partitions
it named. Those are the ones
+ * the refresh is about to record as this MV partition's, and the read is
what has to match the record:
+ * reading the MV partition's own key range instead also reads base
partitions no snapshot describes,
+ * and a later silent change to one of them -- dropped, with the base
partition set back to what it
+ * was -- leaves the rows it put in this MV partition behind while the
partition is still judged
+ * synchronized, so the transparent rewrite serves them and no refresh
plans it again.
+ *
+ * <p>Every other table keeps the MV partition's own key range, which is
what the tables the caller
+ * does not scope were always read through, and a scoped range table keeps
it as well: a partition of
+ * a base range table can hold a row past the MV partition it is rolled up
into, see below. Scoped
+ * tables are olap ones; the partition names are looked up on one, see the
caller.
+ */
private static Map<TableIf, Set<Expression>>
constructTableWithPredicates(MTMV mv,
- Set<String> partitionNames, Map<TableIf, String> tableWithPartKey)
throws AnalysisException {
- Set<PartitionItem> items = Sets.newHashSet();
+ Set<String> partitionNames, Map<TableIf, String> tableWithPartKey,
+ Map<BaseTableInfo, Set<String>> readableBasePartitions) throws
AnalysisException {
+ Set<PartitionItem> mvItems = Sets.newHashSet();
for (String partitionName : partitionNames) {
- PartitionItem partitionItem =
mv.getPartitionItemOrAnalysisException(partitionName);
- items.add(partitionItem);
+ mvItems.add(mv.getPartitionItemOrAnalysisException(partitionName));
}
ImmutableMap.Builder<TableIf, Set<Expression>> builder = new
ImmutableMap.Builder<>();
- tableWithPartKey.forEach((table, colName) ->
- builder.put(table, constructPredicates(items, colName))
- );
+ for (Map.Entry<TableIf, String> entry : tableWithPartKey.entrySet()) {
+ TableIf table = entry.getKey();
+ String colName = entry.getValue();
+ Set<String> readable = readableBasePartitions == null ? null
+ : readableBasePartitions.get(new BaseTableInfo(table));
Review Comment:
IVM 那边能不能直接传空 Map?null 和空 Map 在这里效果一样,调用方统一保证非 null,就能省掉这一层判断。查不到表时仍沿用 MV
范围,查到空 Set 时仍表示不读数据,这两个语义保留就行。
##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVRelatedPartitionDescSyncLimitGenerator.java:
##########
@@ -58,6 +58,21 @@ public void apply(MTMVPartitionInfo mvPartitionInfo,
Map<String, String> mvPrope
Optional<String> dateFormat = config.getDateFormat();
Map<MTMVRelatedTableIf, Map<String, PartitionItem>> res =
Maps.newHashMap();
for (Entry<MTMVRelatedTableIf, Map<String, PartitionItem>> entry :
partitionItems.entrySet()) {
+ if (hasDefaultListPartition(entry.getValue())) {
+ // The window is what makes a refresh read fewer base
partitions than the MV partition's own
+ // key range covers. It cannot be applied to a table whose
list partitions have a default
+ // one: that partition takes the rows no other partition of
the table claims, and which of
+ // those rows belong to an MV partition is decided by their
own key rather than by the
+ // partition they were placed in -- ADD PARTITION can claim a
key without moving the rows the
+ // default partition already holds under it, so which side of
the window a row is on is not
+ // something a predicate on the partition columns can say. A
read that is to keep only part of
+ // the MV partition's key range cannot be written for such a
table, so the key range is kept
+ // whole and the mapping names every partition it covers,
which is what the refresh reads for
+ // it. What is given up is the window for this table, not the
correspondence between the read
+ // and the record.
+ res.put(entry.getKey(), entry.getValue());
Review Comment:
IVM 过程中,LIST 分区的基表的 default 分区插入数据,mv 没有对应的分区,无法执行。收集分区 id 的逻辑走了这里,放行了
default
##########
fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java:
##########
@@ -1599,6 +1599,23 @@ public Map<String, Map<MTMVRelatedTableIf, Set<String>>>
calculatePartitionMappi
public Map<String, Map<MTMVRelatedTableIf, Set<String>>>
calculatePartitionMappings(
Map<List<String>, Set<String>> queryUsedBaseTablePartitionMap,
Map<MvccTableInfo, MvccSnapshot> pinnedSnapshots) throws
AnalysisException {
+ return calculatePartitionMappings(queryUsedBaseTablePartitionMap,
pinnedSnapshots, Sets.newHashSet());
+ }
+
+ /**
+ * The same, with every MV partition no base partition describes added to
{@code undescribedMvPartitions}.
+ *
+ * <p>Such a partition is one the alignment has not caught up with: a base
{@code ADD PARTITION} widens the
+ * keys an MV partition covers, so the alignment replaces it with an empty
partition of the wider keys, and
+ * until that is done its own descriptor is one the base partitions no
longer produce. Reading it as no
+ * table feeding it would write no rows over the rows it holds, so a
refresh has to tell it apart from a
+ * partition that really is fed by nothing -- and the default list
partition of a table is named in every
+ * MV partition's mapping, which is why the mapping being non-empty says
nothing about this.
+ */
+ public Map<String, Map<MTMVRelatedTableIf, Set<String>>>
calculatePartitionMappings(
+ Map<List<String>, Set<String>> queryUsedBaseTablePartitionMap,
+ Map<MvccTableInfo, MvccSnapshot> pinnedSnapshots,
+ Set<String> undescribedMvPartitions) throws AnalysisException {
Review Comment:
这里的 undescribedMvPartitions 也是方法算出来的结果,能不能和映射一起返回?现在一个通过返回值拿到,另一个靠修改传进来的
Set,调用方还得把两份结果再放进 Context。用个简单的结果对象装在一起,读起来会更清楚。
##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVRelatedPartitionDescSyncLimitGenerator.java:
##########
@@ -58,6 +58,21 @@ public void apply(MTMVPartitionInfo mvPartitionInfo,
Map<String, String> mvPrope
Optional<String> dateFormat = config.getDateFormat();
Map<MTMVRelatedTableIf, Map<String, PartitionItem>> res =
Maps.newHashMap();
for (Entry<MTMVRelatedTableIf, Map<String, PartitionItem>> entry :
partitionItems.entrySet()) {
+ if (hasDefaultListPartition(entry.getValue())) {
+ // The window is what makes a refresh read fewer base
partitions than the MV partition's own
+ // key range covers. It cannot be applied to a table whose
list partitions have a default
+ // one: that partition takes the rows no other partition of
the table claims, and which of
+ // those rows belong to an MV partition is decided by their
own key rather than by the
+ // partition they were placed in -- ADD PARTITION can claim a
key without moving the rows the
+ // default partition already holds under it, so which side of
the window a row is on is not
+ // something a predicate on the partition columns can say. A
read that is to keep only part of
+ // the MV partition's key range cannot be written for such a
table, so the key range is kept
+ // whole and the mapping names every partition it covers,
which is what the refresh reads for
+ // it. What is given up is the window for this table, not the
correspondence between the read
+ // and the record.
+ res.put(entry.getKey(), entry.getValue());
Review Comment:
感觉含默认LIST 分区的应该 ban 掉吧,我看物化改写的时候是 ban 掉了,保持设计一致
##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVRelatedPartitionDescSyncLimitGenerator.java:
##########
@@ -58,6 +58,21 @@ public void apply(MTMVPartitionInfo mvPartitionInfo,
Map<String, String> mvPrope
Optional<String> dateFormat = config.getDateFormat();
Map<MTMVRelatedTableIf, Map<String, PartitionItem>> res =
Maps.newHashMap();
for (Entry<MTMVRelatedTableIf, Map<String, PartitionItem>> entry :
partitionItems.entrySet()) {
+ if (hasDefaultListPartition(entry.getValue())) {
+ // The window is what makes a refresh read fewer base
partitions than the MV partition's own
+ // key range covers. It cannot be applied to a table whose
list partitions have a default
+ // one: that partition takes the rows no other partition of
the table claims, and which of
+ // those rows belong to an MV partition is decided by their
own key rather than by the
+ // partition they were placed in -- ADD PARTITION can claim a
key without moving the rows the
+ // default partition already holds under it, so which side of
the window a row is on is not
+ // something a predicate on the partition columns can say. A
read that is to keep only part of
+ // the MV partition's key range cannot be written for such a
table, so the key range is kept
+ // whole and the mapping names every partition it covers,
which is what the refresh reads for
+ // it. What is given up is the window for this table, not the
correspondence between the read
+ // and the record.
+ res.put(entry.getKey(), entry.getValue());
Review Comment:
话说是不是应该禁止创建这种 mv 啊,带默认 list 分区的基表
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java:
##########
@@ -144,15 +170,112 @@ public static Pair<Map<BaseTableInfo, Set<String>>,
Map<BaseColInfo, Set<String>
if (allCompensateIsNull) {
return null;
}
+ // A base partition the query reads that no MV partition of this plan
is named from -- one the
+ // partition_sync_limit window left out, say -- is one the MV does not
answer for: a partition of the MV
+ // reads a table through the partitions it does name, and the mapping
has no entry for this one. Its
+ // rows have to come from the base table, and two things follow from
that, decided apart.
+ //
+ // The base branch of the union reads those partitions. A table the
mapping mentions not at all is
+ // uncovered in every partition the query uses, so it is read for the
query's own partitions there
+ // rather than skipped -- without this the plan would answer for that
table out of the MV alone.
+ //
+ // And the MV partitions this plan uses have to be taken out when they
hold rows of such a partition: a
+ // partition the window has since left out can have been materialized
in one of them by an earlier
+ // refresh, and the union would then count its rows twice. The refresh
snapshot names exactly the
+ // partitions a refresh read into each MV partition, so that is what
this is asked of. (It is not the
+ // keys: a list partition's default one holds the keys no other
partition of the table claims, so a
+ // partition can be read into an MV partition whose keys do not cover
it.)
+ Map<MTMVRelatedTableIf, Set<String>> uncoveredByTable =
Maps.newLinkedHashMap();
+ boolean uncoveredRecorded = false;
+ for (MTMVRelatedTableIf pctTable :
mtmv.getMvPartitionInfo().getPctTables()) {
+ Set<String> queryUsed =
queryUsedBaseTablePartitionMap.get(pctTable.getFullQualifiers());
+ if (CollectionUtils.isEmpty(queryUsed)) {
+ continue;
+ }
+ // The mapping of a table the query's filtering left out entirely
is empty, so a table it does not
+ // mention at all is a table every query-used partition of which
is uncovered.
+ Map<String, Set<String>> tableMapping =
mtmvRelatedTableIfMapMap.getOrDefault(pctTable, Maps.newHashMap());
+ Set<String> named = Sets.newHashSet();
+ for (Set<String> names : tableMapping.values()) {
+ named.addAll(names);
+ }
+ Set<String> uncovered = Sets.difference(queryUsed, named);
+ if (uncovered.isEmpty()) {
+ continue;
+ }
+ uncoveredByTable.put(pctTable, uncovered);
+ Set<String> recorded = Sets.newHashSet();
+ MTMVRefreshSnapshot refreshSnapshot = mtmv.getRefreshSnapshot();
+ if (refreshSnapshot != null) {
+ for (String mvPartitionName :
rewrittenPlanUsePartitionNameSet) {
+
recorded.addAll(refreshSnapshot.getPctSnapshots(mvPartitionName, new
BaseTableInfo(pctTable)));
+ }
+ }
+ if (!Sets.intersection(uncovered, recorded).isEmpty()) {
+ uncoveredRecorded = true;
+ }
+ }
+ // Narrowing a table's read keeps the MV partitions answering for the
rest, and that only holds while
+ // one input of the query reads the tables it narrows: the pass that
turns the compensation into a plan
+ // puts the filters it is given on one join, so two narrowed inputs
read the pairs both filters reach
+ // and lose the mixed ones -- an a_old x b_kept row, say, when A is
narrowed for a partition the
+ // mapping does not name and B for one the MV no longer holds, or when
one table is narrowed while a
+ // self-join reads it through two inputs, both of which the filter
reaches. Nor can a table be left as
+ // it is while the MV partitions that answered for it are taken out.
So the MV partitions are taken
+ // out, and every table the MV is partitioned on is read for the
query's own partitions, whenever more
+ // than one table is narrowed, a single table is narrowed while the
query reads such a table through
+ // more inputs than that, or a refresh read one of the uncovered
partitions into an MV partition.
+ int narrowedTables = 0;
+ for (MTMVRelatedTableIf pctTable :
mtmv.getMvPartitionInfo().getPctTables()) {
+ BaseColInfo pctInfo = pctInfoMap.get(new BaseTableInfo(pctTable));
+ Set<String> queryUsed =
queryUsedBaseTablePartitionMap.get(pctTable.getFullQualifiers());
+ Set<String> stale = pctInfo == null ? null :
baseTablePartitionNeedUnionNameMap.get(pctInfo);
+ boolean narrowedByStalePass = CollectionUtils.isNotEmpty(stale) &&
!stale.containsAll(queryUsed);
+ if (narrowedByStalePass || uncoveredByTable.containsKey(pctTable))
{
+ narrowedTables++;
+ }
+ }
+ boolean takeMvPartitionsOut = uncoveredRecorded || narrowedTables > 1
+ || (narrowedTables > 0 && pctInputsInQuery > narrowedTables);
+ if (!uncoveredByTable.isEmpty()) {
+ for (MTMVRelatedTableIf pctTable :
mtmv.getMvPartitionInfo().getPctTables()) {
+ Set<String> queryUsed =
queryUsedBaseTablePartitionMap.get(pctTable.getFullQualifiers());
+ if (CollectionUtils.isEmpty(queryUsed)) {
+ continue;
+ }
+ BaseColInfo pctInfo = pctInfoMap.get(new
BaseTableInfo(pctTable));
+ if (pctInfo == null) {
+ continue;
+ }
+ if (takeMvPartitionsOut) {
+ baseTablePartitionNeedUnionNameMap.put(pctInfo, new
HashSet<>(queryUsed));
+ continue;
+ }
+ Set<String> uncovered = uncoveredByTable.get(pctTable);
+ if (CollectionUtils.isEmpty(uncovered)) {
+ continue;
+ }
+ baseTablePartitionNeedUnionNameMap
+ .computeIfAbsent(pctInfo, k -> new
HashSet<>()).addAll(uncovered);
+ }
+ if (takeMvPartitionsOut &&
!rewrittenPlanUsePartitionNameSet.isEmpty()) {
+ mvPartitionNeedRemoveNameMap
+ .computeIfAbsent(new BaseTableInfo(mtmv), k -> new
HashSet<>())
+ .addAll(rewrittenPlanUsePartitionNameSet);
Review Comment:
MVScan(没有任何分区) UnionAll 完整原查询的深拷贝
进了优化器会一直递归,直到 memo 爆了
mv 没任分区应该直接拒绝该候选
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]