Conversation
JingsongLi
left a comment
There was a problem hiding this comment.
Avoiding decode/re-encode during eligible append compaction has clear end-to-end value, but the new statistics merger can silently remove matching query results after compaction. Details are inline.
All 31 existing selected compaction/statistics tests passed on the reviewed head. Additional real-Parquet probes verified the fast path hit, committed the compaction, and compared a filtered table scan with normal rewriting: the fast path lost the matching row. I did not independently benchmark the claimed throughput gains.
| } | ||
| if (current instanceof Comparable && candidate instanceof Comparable) { | ||
| Comparable<Object> currentComparable = (Comparable<Object>) current; | ||
| return currentComparable.compareTo(candidate) >= 0 ? current : candidate; |
There was a problem hiding this comment.
[P1] Merge binary bounds using the existing unsigned comparator
BINARY/VARBINARY values deserialize as byte[], which is not Comparable, so pickMax retains the first contributor's bound; pickMin has the same problem. I reproduced this with two one-row BYTES Parquet files containing 0x01 and 0x02. The fast path copies both rows but records manifest bounds [0x01,0x01]. After committing that compaction, a table scan for payload = 0x02 returns no rows because AppendOnlyFileStoreScan prunes the file; normal rewriting returns the row. The row-count guard passes and cannot catch the wrong metadata.
Use Paimon's unsigned binary ordering (as FullSimpleColStatsCollector/SortUtil.compareBinary already do) for both bounds. Add a compaction-commit/filtered-scan regression whose match is in a later contributor, including reverse ordering for the minimum bound.
There was a problem hiding this comment.
thanks for the review, fixed accordingly.
JingsongLi
left a comment
There was a problem hiding this comment.
Reviewed 72b85d7. Requirement fit: SUPPORTED. Implementation: FINDINGS.
Copying eligible Parquet row groups can avoid decode/re-encode work in append compaction. The previous binary-bounds wrong-result finding is fixed: both contributor orders now use the same unsigned ordering as ordinary statistics and predicates, and both independent real-Parquet probes pass. I found a different, narrower issue in unknown bounds for NOT NULL fields: a valid counts-stats table always falls back after already copying the output. Details are inline; this preserves rows but defeats the optimization and adds I/O.
The exact-head scoped Maven package run passed 33 tests without fast-build. Additional probes verified the prior binary fix and reproduced the new counts-stats failure; changing only the stats serializer to nullable fields made the failing fast-path HIT assertion pass with all 20 rows preserved. No object-store performance benchmark or injected filesystem-failure campaign was run. No current CI rollup was available in the inspected head metadata.
|
|
||
| RowType statsRowType = valueStatsCols == null ? rowType : rowType.project(valueStatsCols); | ||
| int fieldCount = statsRowType.getFieldCount(); | ||
| InternalRowSerializer serializer = new InternalRowSerializer(statsRowType); |
There was a problem hiding this comment.
[P2] Serialize statistics with nullable field types
Please make the statistics serializer's fields nullable, as SimpleStatsConverter already does. For a valid INT NOT NULL table with metadata.stats-mode=counts, merged min/max are null, but this serializer uses the original NOT NULL getter and throws while unboxing the bound. I reproduced this with two real Parquet inputs: all 20 rows survive via normal fallback, but the fast-path HIT stays 0; making only the serializer fields nullable produces HIT=1.
Because copier.copy has already completed before buildResult invokes this serializer, each such compaction first writes and deletes a full copied output and then re-reads/re-encodes the inputs. Add a NOT NULL primitive + counts-stats regression asserting that the fast path actually hits, rather than only checking the fallback result.
There was a problem hiding this comment.
Thanks for catching this. Fixed by making the stats row type's fields nullable in SimpleStatsMerger.merge, mirroring SimpleStatsConverter, which covers both the serializer and the field getters.
Added regressions:
SimpleStatsMergerTest#testMergeWithNotNullFieldType: NOT NULL INT + null min/max bounds (counts stats mode). Verified it throws the exact NPE you described without the fix.ParquetFastPathCompactRewriterTest#testFastPathHitWithNotNullColumnAndCountsStatsMode: NOT NULL primitive +metadata.stats-mode=countsend-to-end, assertingHIT_COUNT == 1(no silent fallback) plus row content and null min/max bounds on the compacted file.
…ction Add an opt-in fast path for append-only table compaction on Parquet files: when all eligibility conditions hold, compaction concatenates compressed row groups directly and only rewrites the footer, skipping row decode/re-encode entirely. Any ineligible input (schema/codec mismatch, deletion vectors, row tracking, file index, bloom filter, encryption, Parquet writer v2, partial-column writes, etc.) falls back to the traditional rewrite path. New options (all default off/serial): - append.compaction.row-group-copy.enabled - append.compaction.row-group-copy.preserve-page-index: keep ColumnIndex/OffsetIndex so page-level predicate pruning still works on compacted files, at the cost of reading and rewriting page indexes - append.compaction.row-group-copy.footer-read.parallelism: bounded concurrent footer reads during fast-path prepare Parquet-specific compatibility checks live in paimon-format (ParquetRowGroupCopyChecker); value stats of output files are merged from file-level stats when a file is fully copied and recomputed from row-group metadata for partially copied files.
RowGroupCopyCompactionBenchmark compares REWRITE vs row-group copy (including preserve-page-index and footer-read parallelism variants) across column shapes, codecs, row-group sizes and file sizes, tunable via -DrowGroupCopyBenchmark.* properties. ParquetPageIndexBenchmark measures the read-side effect of dropping vs preserving the page index under point and range predicates. Neither runs in CI by default (class names do not match surefire patterns; trigger explicitly with -Dtest).
byte[] is not Comparable, so row-group-copy compaction kept the first contributor's min/max and could prune matching rows after commit.
Co-authored-by: Cursor <cursoragent@cursor.com>
…odule. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the test-scoped parquet-hadoop dependency that conflicted with the shaded paimon-format jar when running paimon-core tests without -am. Co-authored-by: Cursor <cursoragent@cursor.com>
9b37f1e to
2e599bd
Compare
Thanks for the review. This PR is now limited to the minimal, default-off fast path. Still in this PR:
Not in this PR; they will be a follow-up after this one is merged:
The branch has been rebased onto current master. |
Co-authored-by: Cursor <cursoragent@cursor.com>
JingsongLi
left a comment
There was a problem hiding this comment.
Reviewed head 2e599bd502 end to end: the new opt-in append-table Parquet row-group copy path, eligibility/fallback checks, output footer/statistics, pruning and integration behavior. This is a useful feature and should remain open. The recent scope reduction and shaded-Parquet test changes are improvements.
[P2] Close the fast-path metric group with the writer. BaseAppendFileStoreWrite.withMetricRegistry now always constructs CompactionFastPathMetrics (14 counters), including when append.compaction.row-group-copy.enabled=false, but BaseAppendFileStoreWrite.close() only closes blobFetchMetrics. CompactionFastPathMetrics has no close method. In Flink, createTableMetricGroup registers a child group in the task's metric system; each writer lifecycle leaves this group registered after write.close(). I reproduced this with a tracking MetricRegistry: create a normal append writer, call withMetricRegistry, close the writer, and assert that the compactionFastPath group closed. The focused probe fails (1 test, 1 failure); the adjacent BlobFetchMetrics follows the expected close pattern. Please make this metric holder closeable, close it from the writer, and cover that lifecycle (preferably also avoiding registration when the feature is disabled).
Verification on JDK 8: ParquetFastPathCompactRewriterTest 14/14, SimpleStatsMergerTest 6/6, AppendOnlyTableCompactionTest 11/11, and adjacent Parquet format/stats tests 33/33 passed. These exercise actual copied row groups, row reads, predicate pruning and stats. The temporary lifecycle probe was removed after reproduction; the review checkout is clean.
Release gate: this branch currently reports no GitHub checks. Please run the affected core/format CI on the revised head before merge, including the metric lifecycle fix.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Rechecked current head |
…r only when enabled. CompactionFastPathMetrics is now closeable and closed from BaseAppendFileStoreWrite.close(), and withMetricRegistry only registers the metric group when append.compaction.row-group-copy.enabled is true, so writer lifecycles no longer leak a registered metric group when the fast path is disabled.
Thanks for the detailed review. Both points addressed in Metric lifecycle — fixed as suggested:
Added
Verification on JDK 8: |
|
Reviewed head The opt-in Parquet row-group copy path has a concrete compaction benefit. I checked eligibility and fallback, copied footer and value-stat handling, output cleanup, and the writer metric lifecycle. The previous metric-group leak is fixed: registration is conditional on the option, close propagates to the group, and both behaviors have focused regressions. Verification on JDK 8 / Maven 3.9.9: The current CI run has one failure in |
Co-authored-by: Cursor <cursoragent@cursor.com>
…hanBlobsProcedureTest shuffle cleanup)
Purpose
close #9664
Compaction of append-only tables rewrites every data file, even when inputs are mergeable as-is. Since Parquet row groups are self-contained compressed units, files sharing the same schema and codec can be merged by concatenating row groups and rewriting only the footer, skipping row decode/re-encode entirely.
This PR adds an opt-in row-group copy fast path to append-only compaction, controlled by
append.compaction.row-group-copy.enabled(defaultfalse). Each compaction batch is checked for eligibility (Parquet format, current schema, uniform codec, no deletion vectors / row tracking / file index / encryption, etc.); any ineligible file makes the whole batch fall back to the traditional rewrite path, so it is always safe to enable. Parquet-specific checks live in paimon-format (ParquetRowGroupCopyChecker), keeping paimon-core free of Parquet internals.Two companion options:
append.compaction.row-group-copy.preserve-page-index(defaultfalse) keeps ColumnIndex/OffsetIndex on compacted files at the cost of extra reads, andappend.compaction.row-group-copy.footer-read.parallelism(default1, hard cap 8) bounds concurrent footer reads.Benchmarks (
RowGroupCopyCompactionBenchmark, included): 6.4–6.9× on narrow numeric tables, up to 24–32× on wide string tables (zstd, 8MB row groups); production Flink/Spark compaction jobs saw a stable ~59–68% reduction in kernel task time with zero fallbacks.Documentation:
core_configuration.htmlregenerated, plus a new section in the append-table docs.Tests
ParquetFastPathCompactRewriterTest: fast-path hit, all fallback conditions, partial file copy stats merging, row-count verification.SimpleStatsMergerTest: stats merging across files and row groups.