yujun777 commented on code in PR #68648:
URL: https://github.com/apache/doris/pull/68648#discussion_r4269846403
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java:
##########
@@ -130,17 +142,90 @@ 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));
+ if (readable == null) {
+ builder.put(table, constructPredicates(mvItems, colName));
+ continue;
+ }
+ OlapTable olapTable = (OlapTable) table;
+ Set<PartitionItem> items = Sets.newHashSet();
+ for (String partitionName : readable) {
+
items.add(olapTable.getPartitionItemOrAnalysisException(partitionName));
+ }
+ if (items.stream().anyMatch(PartitionItem::isDefaultPartition)) {
+ // One of the partitions this MV partition is recorded with is
a list partitioned table's
+ // default partition, which takes the rows no other partition
of it claims. Those rows are
+ // the ones the MV partition's own key range names, wherever
the base table put them, and a
+ // partition of the MV takes them by that key rather than by
the partition they were placed
+ // in. So a table whose mapped partitions include one is read
the way an unscoped one is:
+ // the MV partition's key range, at the partition column's own
type. Which of the rows that
+ // range reaches sit in an explicit partition and which in the
default one is not something a
+ // predicate on the partition columns can say -- ADD PARTITION
claims a key without moving the
+ // rows a default partition already holds under it -- so this
table is not scoped at all, and
+ // the mapping is kept whole for it for the same reason: see
+ // MTMVRelatedPartitionDescSyncLimitGenerator, which leaves
such a table out of the window.
+ // The mapping names the default partition in every MV
partition that reads the table, so this
+ // is reached for each of them and not only for the one the
sentinel key maps to.
+ builder.put(table, constructPredicates(mvItems, colName,
+ Optional.of(partitionColumnType(olapTable, colName))));
+ continue;
+ }
+ if (readable.isEmpty()) {
+ // No partition of this table feeds the MV partitions being
refreshed, which is "no row"
+ // rather than "every row": constructPredicates answers the
other way for an empty set,
+ // and that answer would put every row of the table into each
of them.
+ builder.put(table, Sets.newHashSet(BooleanLiteral.FALSE));
+ continue;
+ }
+ if (items.iterator().next() instanceof ListPartitionItem) {
+ // A key of a list partition is a value the MV partition's own
key set was written from, so
+ // the whole key pinned here is inside the range this batch
writes and nothing is added to it.
+ builder.put(table, constructPredicatesOfBasePartitions(items,
olapTable, colName));
+ continue;
+ }
+ // A partition of a base range table can end past the MV partition
it is rolled up into, and the
+ // roll-up keeps such a partition: MTMVPartitionExprDateTrunc
takes the last second off the base's
+ // upper bound before truncating it, so a DATETIME(3) partition
ending at 2025-01-01 00:00:00.123
+ // is rolled up into the year 2024. This batch writes the year
2024 partition alone, and a row of
+ // that partition just past its upper bound -- 2025-01-01
00:00:00.100 -- is then read by a batch
+ // that has no partition to put it in: the sink counts it as
filtered and `enable_insert_strict`,
+ // on by default, fails the refresh for it. So the mapped
partitions' bounds are read together
+ // with the MV partitions' own ranges -- the term every other
table is read through -- and what is
+ // read is the rows of the recorded partitions that these MV
partitions hold.
+ Expression mvRange =
ExpressionUtils.or(constructPredicates(mvItems, colName));
+ Set<Expression> scoped = Sets.newHashSet();
+ for (Expression partition :
constructPredicatesOfBasePartitions(items, olapTable, colName)) {
+ scoped.add(ExpressionUtils.and(partition, mvRange));
Review Comment:
Fixed in 0317395556b. You are right, and the severity is the silent one: the
MV side of the scoped RANGE read was built through
`constructPredicates(mvItems, colName)`, i.e. from the MV partition's key, and
a key carries only its primitive type -- `Type.fromPrimitiveType(DATETIMEV2)`
is scale 0. For a direct `PARTITION BY ts` MV over `[2024-01-01 00:00:00.123,
2024-02-01 00:00:00.123)` that made the read's upper bound `2024-02-01
00:00:00.000`, so the last 123 milliseconds of the partition were not read, and
the row at `2024-02-01 00:00:00.100` -- which the batch does write -- never
reached the MV. The refresh succeeds either way, so the only trace was the
missing row.
The MV side is now written at the base partition column's type, as the base
side already is. Where the MV partition's bounds are the base partition's (this
shape) the two sides are the same expression, so the predicate still collapses
to one term pair.
Measured both ways on a local cluster with the new
`mtmv_p0/test_mtmv_base_partition_read_scope` case `range_ms_direct` (direct
range MV over that partition, rows at `2024-01-01 00:00:00.123` and `2024-02-01
00:00:00.100`):
* with the MV side written from the key: the refresh task succeeds and the
golden check reports `Check tag 'range_ms_direct' failed, line 2 mismatch, real
line is empty, but expect is 2024-02-01T00:00:00.100 2` -- the row is simply
not there;
* with the type: both rows are in the MV and the suite passes.
`UpdateMvByPartitionCommandTest#testADirectRangeMvKeepsItsOwnBoundAtTheColumnScale`
pins it at the predicate (the bound carries the milliseconds and no bound is
written without them) and fails with the MV side written from the key; 15 tests
pass, and `MTMVRewriteUtilTest`, `MTMVTest`, `MTMVPartitionUtilTest`,
`MTMVTaskTest`, `MTMVRelatedPartitionDescGeneratorTest`,
`ExplainRefreshMTMVCommandTest`, `RefreshMTMVCommandTest` and
`PartitionCompensatorTest` pass as well.
One disclosure on the scope of this: the read of a table the refresh does
not scope -- an external base table, or the full/incremental read of an IVM MV
-- still writes the MV partition's bound from the key, i.e. it has the same
gap. That path is unchanged from master (this PR scopes olap tables only), and
closing it needs the same conversion for a PCT table that is not an
`OlapTable`; I have left it out rather than widen this PR without a test for
it, and can send it as a follow-up.
--
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]