zhuxiangyi opened a new pull request, #9659:
URL: https://github.com/apache/paimon/pull/9659

   ### Purpose
   
   Paimon cannot answer "how many data files does this table have, and how 
large are they" without folding every manifest entry.
   
   Two places pay for it today:
   
   - `ANALYZE TABLE` calls `listPartitionEntries()` and sums `fileSizeInBytes` 
over the whole manifest set purely to obtain one number, then uses it to 
*estimate* the merged size as `totalSize * mergedRecordCount / 
totalRecordCount` (`PaimonAnalyzeTableColumnCommand.scala:62-73`).
   - Append compaction planning calls `partitionEntries()` on every discovery 
round — `AppendTableCompact.java:115`, `CombinedAppendCompactSource.java:181`, 
`MultiTableAppendCompactReadOperator.java:110` — which in combined mode is one 
full manifest scan per table per round.
   
   The commit path already computes this information and then throws it away. 
`FileStoreCommitImpl` builds `PartitionEntry.merge(deltaFiles)` on every 
commit, whose counts are already signed by file kind, hands the result to the 
catalog as partition statistics, and drops it for every catalog that does not 
implement partition reporting — `AbstractCatalog.commitSnapshot` throws 
`UnsupportedOperationException`, and `RenamingSnapshotCommit.commit` ignores 
the argument.
   
   Summing those per-partition entries gives the table level delta, so a 
snapshot can carry the running totals:
   
   - `numFiles` — live data files
   - `totalFileSizeInBytes` — their total size
   
   Both are derived as `previous + delta`. This adds no IO and no second pass 
over the delta entries; it reuses a fold that already runs.
   
   Deriving the totals costs nothing when it works and is skipped when it does 
not, so the values are advisory. `null` means unknown, never zero, and readers 
fall back to scanning — `PaimonAnalyzeTableColumnCommand` shows the intended 
shape:
   
   ```scala
   val totalSize = Option(currentSnapshot.totalFileSizeInBytes())
     .map(_.longValue())
     
.getOrElse(table.newScan().listPartitionEntries().asScala.map(_.fileSizeInBytes()).sum)
   ```
   
   Unknown arises for snapshots written before the fields existed, and for any 
commit whose predecessor was unknown. Two paths recompute instead of 
propagating it:
   
   - `replaceManifestList` replaces the manifest layout wholesale and its 
callers may drop files with it — `RemoveUnexistingManifestsAction` does exactly 
that — so inheriting the previous counters would publish a number that no 
longer matches the live file set. It folds the manifests it is about to commit 
instead.
   - Manifest compaction seeds unknown counters from the compacted manifest 
set. It rewrites the whole manifest set anyway and the compacted form is the 
cheapest one to fold, which makes `compact_manifest` the way to recover the 
counters on an upgraded table; the incremental chain continues on its own 
afterwards. To keep that reliable the procedure commits a snapshot when the 
counters are unknown even if no manifests need merging, and returns early 
exactly as before once they are known.
   
   The two-field, fold-at-commit, advisory-and-degradable shape follows Delta 
Lake's version checksum file, which maintains the equivalent counters per 
commit and falls back rather than failing when it cannot.
   
   ### Benefits
   
   Delivered here:
   
   - `ANALYZE TABLE` no longer scans the manifest set for the table size, and 
the value it feeds into the merged-size calculation is exact rather than a scan 
result summed on the spot.
   - `sys.snapshots` gains `num_files` and `total_file_size_in_bytes`, so 
"which commit blew up the file count" is answerable by query, for any retained 
snapshot, without starting a job and without scanning manifests. Commit metrics 
cannot answer this: they are per-commit deltas held in a running writer's 
memory, carry no snapshot identity, and are absent entirely for tables written 
by Spark batch jobs or procedures.
   
   Enabled next, not part of this PR:
   
   - Compaction planning and cost-based optimization can read the counters 
instead of scanning, with the fallback above keeping them correct on tables 
that have not been seeded.
   - The same fold extends naturally to a file size histogram and to deletion 
vector counts, which would let compaction decisions move from a file count 
threshold to a distribution.
   
   ### Compatibility
   
   Existing tables are unaffected. The fields are nullable and serialized only 
when present, so snapshots written before this change stay byte-identical, and 
the constructors that predate the fields are kept so no caller needs to change. 
Reads, writes, compaction, overwrite, tags, rollback, snapshot expiration and 
the `sys.snapshots` query all keep working on such tables, with the two columns 
reported as `NULL`.
   
   ### Tests
   
   `SnapshotFileStatsTest`, 20 cases:
   
   - Five assert the values against a full manifest scan across appends, 
primary key compaction, overwrite, tag and rollback, and manifest compaction.
   - Eleven cover tables rewritten into the pre-change snapshot format — read, 
write, primary key compaction, overwrite, tag, rollback, snapshot expiration, 
the `sys.snapshots` columns, mixed old/new history with time travel, and 
unknown JSON fields. The central one asserts that writing on top of an unknown 
baseline keeps reporting unknown rather than restarting from zero, which is the 
failure mode that would publish a plausible but wrong file count.
   - The rest cover the recompute paths, including one that drops a manifest 
and must follow the shrink instead of reporting the stale count, and seeding a 
legacy table whose manifests need no merging at all.
   
   `RemoveUnexistingManifestsActionITCase` and `CompactManifestProcedureITCase` 
exercise the same paths through Flink.
   
   Full run: 293 paimon-core cases and 9 paimon-flink cases, no failures. 
`paimon-spark-common` compiles.
   
   ### Documentation
   
   `docs/docs/concepts/system-tables.mdx` documents the two columns, what 
`NULL` means, how it propagates, and that `compact_manifest` recovers the 
values.
   


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

Reply via email to