morningman opened a new pull request, #69778:
URL: https://github.com/apache/doris/pull/69778

   ### What problem does this PR solve?
   
   Issue Number: None
   
   Related PR: None
   
   Problem Summary:
   
   **In short.** An IVM materialized view silently misses the rows of a base 
partition its internal stream has no offset for (a partition added after the MV 
was built, or one that was still empty then) when those rows are older than the 
base table's `changelog.ttl_seconds` at refresh time. The refresh succeeds, the 
rows are never read, and a base-table query of the MV's shape that transparent 
rewrite answers from the MV returns the same wrong result. This PR makes an IVM 
read take such a partition from the base table, as an append of all its rows, 
instead of from its changelog.
   
   **Background.** An IVM MV reads each base table through an internal table 
stream. The stream keeps an offset per partition and records one only when it 
consumes the partition with data in it. It is created with `show_initial_rows`, 
so a partition that has data at creation is a historical partition, read once 
from the base table; a partition that is empty then gets nothing; and every 
consumption, the reset of a COMPLETE refresh included, records offsets only for 
the partitions it read with data. Two kinds of partitions therefore have no 
offset and none of their rows in the MV: a partition added after the stream was 
created, and a partition that was empty when the stream was created and at 
every consumption since (a range partition created ahead of its data is the 
usual one).
   
   **The problem, and what it cost.**
   - `NormalizeOlapTableStreamScan.makeTableStreamScan` reads a partition with 
no offset like any other incremental partition: from its changelog, with no 
start. `applyChangelogTtl` turns the missing start into the first TSO the 
changelog retains and does not reject it.
   - Once the partition's rows are older than the TTL when the MV is refreshed, 
the read skips them. The refresh succeeds and records the partition's offset 
past them, so no later refresh reads them either: the MV stays short of those 
rows for good.
   - This holds for an MV over a unique-key table and for an append-only detail 
MV over a duplicate-key table. An MV partitioned like its base table rebuilds 
the MV partition of an added base partition, but its partition that was empty 
when the MV was built is caught up incrementally and loses its rows the same 
way.
   
   **How this PR fixes it.**
   - In an IVM statement, `makeTableStreamScan` moves the incremental 
partitions that have no consumption baseline 
(`OlapTableStreamWrapper.filterConsumedPartitionIds`: no offset, or the 
sentinel offset of a partition empty at stream creation) to the branch that 
reads historical partitions from the base table: every row as an append, with 
the commit TSO as the sequence and the row LSN as the row identity. For an MV 
that holds none of the partition's rows that is the partition's whole change, 
whatever the TTL. The offset the refresh records afterwards is the same as 
before.
   - The base read and the end TSO the refresh commits come from the same 
snapshot, as for a historical partition: in shared-nothing mode both are taken 
under the table read lock that planning holds, and in cloud mode the base read 
is pinned to the visible version of the stream read state.
   - A user stream is not changed: reading the changelog it retains, from the 
first retained TSO, is what it promises. The choice is made by the statement 
being an IVM rewrite, not by the stream.
   
   **Results** (local shared-nothing cluster; rows written to an added 
partition and to the partition that was empty, older than a one-second TTL when 
the MV is refreshed):
   
   | MV | Before | After |
   | --- | --- | --- |
   | non-partitioned, over a unique-key table | SUCCESS, the rows of both 
partitions missing for good | SUCCESS, the MV matches the base table |
   | append-only detail MV, over a duplicate-key table | SUCCESS, the rows of 
both partitions missing for good | SUCCESS, the MV matches the base table |
   | partitioned like its unique-key base table | SUCCESS, the added partition 
rebuilt, the rows of the one that was empty missing | SUCCESS, the MV matches 
the base table |
   | the next refresh, after more changes, with the TTL back to a day | 
incremental, the rows still missing | incremental, the new changes read from 
the changelog |
   
   **Classes, and how they call each other.**
   - `IvmIncrRefreshMTMV` → `IvmDeltaRewriteState.createDeltaScan` builds a 
`LogicalOlapTableStreamScan` over an `OlapTableStreamWrapper`.
   - `NormalizeOlapTableStreamScan.makeTableStreamScan` → 
`filterHistoryPartitionIds` / `filterIncrementalPartitionIds`, and in an IVM 
statement `filterConsumedPartitionIds` → historical and unconsumed partitions: 
`makeOlapScanOnBaseTable`; consumed partitions: 
`makeIncrementalScanFromChangelog` → `applyChangelogTtl`.
   - `StreamConsumptionInfoExtractor` records the new offsets from the 
wrapper's output map, as before.
   
   Not changed: the TTL check, the snapshot and reset reads (the snapshot read 
already leaves the partitions without a baseline out and reports that it did), 
user streams, and anything persisted.
   
   ### Release note
   
   Fix an IVM materialized view missing the rows of a base partition that was 
added after the MV was created, or was empty then, when those rows were older 
than the table's `changelog.ttl_seconds` at refresh time.
   
   ### Check List (For Author)
   
   - Test: Regression test / Unit Test
       - Regression test: `mtmv_p0/ivm/test_ivm_partition_without_offset` (new, 
nonConcurrent): a non-partitioned MV over a unique-key table, an append-only 
detail MV over a duplicate-key table and an MV partitioned like its unique-key 
base table, each refreshed after rows written to an added partition and to the 
partition that was empty became older than the TTL, then again after more 
changes with the TTL back to a day, and compared with the base table with MV 
rewrite off. Local shared-nothing cluster: first on a build without this 
change, where all three refreshes succeeded without those rows; with the change 
the suite passes, and passes again against the generated `.out`. On the same 
build the whole `mtmv_p0/ivm` directory (113 suites), `mtmv_ivm_restart_p0` and 
`changelog_p0/test_changelog_before_column_name` pass. After the rebase onto 
the latest master (two commits that touch neither this code nor IVM), the new 
suite and the other `test_ivm_partition_*` suites pass again. Not r
 un on a cloud cluster.
       - Unit Test: `ExplainTableStreamPlanTest` (two new cases: a user stream 
reads a partition without a consumption baseline from the changelog; the same 
read in an IVM statement takes it from the base table and still reads the 
partition with an offset from the changelog), run together with 
`IvmIncrRefreshMTMVTest`, `IvmFullRefreshMTMVTest`, 
`InsertIntoTableCommandTableStreamTest`, `UpdateMvByPartitionCommandTest` and 
`CollectRelationChangelogTtlTest`: 84 tests pass (before the rebase).
   - Behavior changed: Yes. An IVM refresh reads a partition with no stream 
offset from the base table instead of from its changelog; for such a partition 
`EXPLAIN REFRESH ... INCREMENTAL` shows a scan of the base table.
   - Does this need documentation: No
   


-- 
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]

Reply via email to