github-actions[bot] commented on code in PR #69028:
URL: https://github.com/apache/doris/pull/69028#discussion_r4336013846


##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/AnalysisManager.java:
##########
@@ -1276,6 +1276,45 @@ public TableStatsMeta findTableStatsStatus(long tblId) {
         return idToTblStats.get(tblId);
     }
 
+    /**
+     * The record of the table, created in the state of a table whose row 
count has never been collected when
+     * it doesn't exist yet. A truncation, a whole table DROP STATS and a 
schema change remove the record, so
+     * the rows which are loaded afterwards can only be turned into the row 
count of the table while the
+     * record exists: the planner falls back to it exactly while the backends 
have not reported the row count
+     * of the new tablets yet, which is the interval right after those 
operations.
+     *
+     * <p>Loads run this from the transaction visibility of the load, both on 
the master
+     * ({@code DatabaseTransactionMgr.updateCatalogAfterVisible}) and while a 
follower replays the transaction
+     * state, so the record exists on every frontend without a journal entry 
of its own.
+     *
+     * @return the existing or the created record, or null when the table 
cannot be resolved any more
+     */
+    private TableStatsMeta findOrCreateTableStats(InternalCatalog catalog, 
long dbId, long tableId) {
+        TableStatsMeta tableStats = idToTblStats.get(tableId);
+        if (tableStats != null) {
+            return tableStats;
+        }
+        Optional<Database> dbOption = catalog.getDb(dbId);
+        if (!dbOption.isPresent()) {
+            return null;
+        }
+        Optional<Table> tableOption = dbOption.get().getTable(tableId);
+        if (!tableOption.isPresent() || !(tableOption.get() instanceof 
OlapTable)) {
+            return null;
+        }
+        return findOrCreateTableStats((OlapTable) tableOption.get());
+    }
+
+    private TableStatsMeta findOrCreateTableStats(OlapTable table) {
+        TableStatsMeta tableStats = idToTblStats.get(table.getId());
+        if (tableStats != null) {
+            return tableStats;
+        }
+        TableStatsMeta created = new TableStatsMeta(table);
+        TableStatsMeta existing = idToTblStats.putIfAbsent(table.getId(), 
created);

Review Comment:
   [P2] Preserve the stats-map lock when inserting load records
   
   `writeTableStats()` holds the `idToTblStats` monitor while serializing its 
size and entries, but this insertion bypasses that monitor. During a live 
metadata dump (`MetaService.dump()` -> serving `Env.dumpImage()`), a cloud BE's 
`reportCommitTxnResult()` can reach this helper without a table lock, despite 
the dump's table read locks. If it inserts a first-load record after the size 
is written but before the iterator is created, the dump declares N records and 
emits N+1. `readIdToTblStats()` then leaves the extra JSON in the stream and 
the next module reader consumes it, making the image unreadable. This concerns 
live dumps; ordinary checkpoints use an isolated Env. Use the same map monitor 
as the existing publication/removal helpers, or serialize a consistent snapshot.



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/TableStatsMeta.java:
##########
@@ -268,6 +382,29 @@ public long getBaseIndexDeltaRowCount(OlapTable table) {
         return updatedRows.get() - maxUpdateRows;
     }
 
+    /**
+     * The row count of the index together with the rows loaded since it was 
collected, i.e. the row count of
+     * the table. Both are read by the planner without the table lock, for 
instance while it plans a direct
+     * scan of a materialized view, so they have to come from the same state 
of this record: a collected row
+     * count paired with the baseline of another analysis would count rows 
twice or miss them. The rows loaded
+     * while this call runs are not part of the snapshot, whichever state 
reads them accumulates them in
+     * {@link #updatedRows} and reports them as delta rows.
+     */
+    public synchronized long getRowCountWithDeltaRows(OlapTable table, long 
indexId) {
+        long rowCount = getRowCount(indexId);
+        if (!keepsOneRowPerBaseRow(table, indexId)) {
+            // The index aggregates, or it merges the rows of a unique key 
table: it has its own, smaller row
+            // count, and the rows loaded into the base index would overstate 
it.
+            return rowCount;
+        }
+        if (rowCount == TableIf.UNKNOWN_ROW_COUNT) {

Review Comment:
   [P2] Keep a usable estimate for a newly added projection rollup
   
   On a DUP table `t(k1,k2,v)`, load 5 rows and ANALYZE, then add `r_dup(k1,v)` 
and load 100 more rows before the new rollup tablets report their counts. ADD 
ROLLUP retains the existing `TableStatsMeta`, so it has a collected base count 
and delta=100 but no `r_dup` entry. The plan `Project(k1,v) -> OlapScan(t INDEX 
r_dup)` passes `keepsOneRowPerBaseRow()`, yet this branch returns -1 and 
`computeOlapScan()` clamps it to 1 although the rollup holds 105 rows. 
Previously the fallback returned -1+100=99. For a row-preserving index without 
its own snapshot, use the known base count and delta, or preserve the usable 
load estimate. Add coverage for ANALYZE -> ADD ROLLUP -> LOAD before the first 
new-index report. This needs neither injected stats nor DROP STATS.



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/AnalysisManager.java:
##########
@@ -1366,52 +1405,52 @@ public void replayUpdateRowsRecord(UpdateRowsEvent 
event) {
         // Record : TableId -> (TabletId -> update rows)
         if (event.getTabletRecords() != null) {
             for (Entry<Long, Map<Long, Long>> record : 
event.getTabletRecords().entrySet()) {
-                TableStatsMeta statsStatus = idToTblStats.get(record.getKey());
-                if (statsStatus != null) {
-                    Optional<Database> dbOption = 
catalog.getDb(event.getDbId());
-                    if (!dbOption.isPresent()) {
-                        LOG.warn("Database {} does not exist.", 
event.getDbId());
-                        continue;
-                    }
-                    Database database = dbOption.get();
-                    Optional<Table> tableOption = 
database.getTable(record.getKey());
-                    if (!tableOption.isPresent()) {
-                        LOG.warn("Table {} does not exist in DB {}.", 
record.getKey(), event.getDbId());
-                        continue;
-                    }
-                    Table table = tableOption.get();
-                    if (!(table instanceof OlapTable)) {
-                        continue;
-                    }
-                    OlapTable olapTable = (OlapTable) table;
-                    short replicaNum = 
olapTable.getTableProperty().getReplicaAllocation().getTotalReplicaNum();
-                    Map<Long, Long> tabletRows = record.getValue();
-                    if (tabletRows == null || tabletRows.isEmpty()) {
-                        LOG.info("Tablet row count map is empty");
-                        continue;
-                    }
-                    long rowsForAllReplica = 0;
-                    for (Entry<Long, Long> entry : tabletRows.entrySet()) {
-                        rowsForAllReplica += entry.getValue();
-                        if (LOG.isDebugEnabled()) {
-                            LOG.debug("Table id {}, tablet id {}, row count 
{}",
-                                    record.getKey(), entry.getKey(), 
entry.getValue());
-                        }
-                    }
-                    long tableUpdateRows = rowsForAllReplica / replicaNum;
-                    LOG.info("Update rows for table {} is {}, replicaNum is 
{}, "
-                            + "rows for all replica {}, tablets count {}",
-                            olapTable.getName(), tableUpdateRows, replicaNum, 
rowsForAllReplica, tabletRows.size());
-                    statsStatus.updatedRows.addAndGet(tableUpdateRows);
-                    if (StatisticsUtil.enablePartitionAnalyze()) {
-                        updatePartitionRows(olapTable, tabletRows, 
statsStatus, replicaNum);
+                Optional<Database> dbOption = catalog.getDb(event.getDbId());
+                if (!dbOption.isPresent()) {
+                    LOG.warn("Database {} does not exist.", event.getDbId());
+                    continue;
+                }
+                Database database = dbOption.get();
+                Optional<Table> tableOption = 
database.getTable(record.getKey());
+                if (!tableOption.isPresent()) {
+                    LOG.warn("Table {} does not exist in DB {}.", 
record.getKey(), event.getDbId());
+                    continue;
+                }
+                Table table = tableOption.get();
+                if (!(table instanceof OlapTable)) {
+                    continue;
+                }
+                OlapTable olapTable = (OlapTable) table;
+                TableStatsMeta statsStatus = findOrCreateTableStats(olapTable);

Review Comment:
   [P2] Order record recreation with DROP STATS journaling
   
   The `VISIBLE` transaction is durable before 
`DatabaseTransactionMgr.finishTransaction()` calls 
`updateCatalogAfterVisible()`. A concurrent whole-table `DROP STATS t` takes no 
target-table lock, so it can remove the record, journal `DELETE_TABLE_STATS`, 
and finish remote invalidation in that gap. Resuming here creates a record 
containing the load's N rows on the master. Followers and the checkpoint 
instead replay `VISIBLE` (create N) followed by `DELETE_TABLE_STATS` (remove 
it), leaving no record. Before tablet reporting, `Project(*) -> OlapScan(t)` 
consequently estimates N on the master but 1 after replay or failover. The 
previous get-only path could not resurrect the deleted record. Order 
creation/removal consistently with the transaction and deletion journals, and 
cover this interleaving with deterministic barriers.



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