perf(parquet): forward dictionary encoding through append compaction rewrite - #257
Open
SteNicholas wants to merge 1 commit into
Open
perf(parquet): forward dictionary encoding through append compaction rewrite#257SteNicholas wants to merge 1 commit into
SteNicholas wants to merge 1 commit into
Conversation
SteNicholas
force-pushed
the
PAIMON-230
branch
4 times, most recently
from
August 28, 2026 08:00
42bdc18 to
269f33f
Compare
Collaborator
|
LGTM |
…rewrite An append-only compaction rewrite copies rows into the new file without inspecting any value, so expanding a dictionary-encoded Parquet column on read and hashing it again on write is work neither side needs. Forward the encoding instead. Reader: `parquet.read.enable-dictionary-passthrough`, off by default, makes ParquetFileBatchReader request `set_read_dictionary` for non-nested STRING/BINARY columns whose every data page, in every row group of the file, is dictionary-encoded. A dictionary page alone cannot be the signal - a column that outgrew its page limit still carries the page it had already emitted - so the gate reads the encoding statistics. Writer: ParquetFormatWriter recovers each batch's encoding from its layout, because exporting through the Arrow C data interface drops the type. A layout pins down neither the index nor the offset width, so only `dictionary(int32, utf8|binary)` is recoverable; CompactRewrite decodes anything else per column while the type is still known - the ORC reader's `dictionary(int64, large_utf8)` under lazy decoding, dictionaries below the top level - and leaves the rest encoded. A dictionary holding nulls in its values is flattened at the writer, the one shape parquet::arrow rejects outright. Compaction opts in only when the output is Parquet, `parquet.enable-dictionary` is on and no shredding plan is active; anything else forces the read option off. A file index on a forwarded column materializes that column alone. Note that a Parquet column chunk carries one dictionary, so when the input files supply different ones the output keeps the first and falls back to plain for the rest of the row group. The rewritten data is unchanged, but the output file may be larger than one written from materialized values.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
August 28, 2026 12:27
269f33f to
92d8218
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Linked issue: close #230
An append-only compaction rewrite copies rows into the new file without inspecting any value, so expanding a dictionary-encoded Parquet column on read and hashing it again on write is work neither side needs. This forwards the encoding instead.
Reader. New option
parquet.read.enable-dictionary-passthrough, off by default, makesParquetFileBatchReaderrequestset_read_dictionaryfor non-nestedSTRING/BINARYcolumns whose every data page, in every row group of the file, is dictionary-encoded. A dictionary page alone cannot be the signal — a column that outgrew its page limit still carries the page it had already emitted — so the gate reads the encoding statistics. The file schema keeps reporting the logical type; only the emitted batches carry the dictionary.Writer.
ParquetFormatWriterrecovers each batch's encoding from its layout, because exporting through the Arrow C data interface drops the type. A layout pins down neither the index nor the offset width, so onlydictionary(int32, utf8|binary)— exactly what Arrow's Parquet reader emits — is recoverable.CompactRewritetherefore decodes anything else per column, while the type is still known: the ORC reader'sdictionary(int64, large_utf8)under lazy decoding, dictionaries below the top level, non-binary value types. The rest stay encoded, so one awkward column does not cost the others their encoding. A dictionary holding nulls in its values is flattened at the writer — the one shapeparquet::arrowrejects outright.Gating. Compaction opts in only when the output is Parquet,
parquet.enable-dictionaryis on, and no shredding plan is active; anything else forces the read option off regardless of the table setting. A file index configured on a forwarded column materializes that column alone.Known trade-off. A Parquet column chunk carries one dictionary, so when the input files supply different ones the output keeps the first and falls back to plain for the rest of the row group. The rewritten data is unchanged, but the output file may be larger than one written from materialized values. This is documented in
compaction.rstand pinned byTestWriteDictionaryChangingAcrossBatches. Whether it is worth mitigating (for example by starting a new row group at each dictionary boundary) should be decided from the benchmark numbers below.Design follows Velox's
perf(parquet): Dictionary passthrough and selective flattening in Parquet writer(facebookincubator/velox#17986): per-column selective flatten, only VARCHAR/VARBINARY passed through, dictionaries with null values flattened, import schema reconciled per batch. It differs in where the flatten happens — Velox still holds a typedVectorinside its writer, whereasFormatWriter::AddBatch(ArrowArray*)here receives an untyped array, so the flatten has to run one layer up, before the type is dropped.Tests
Unit:
ArrowUtilsTest.TestResolveParquetDictionaryStructType— layout-derived resolution; rejects non-STRING/BINARYvalue types,large_utf8, and dictionaries below the top level; preserves a caller-declared dictionary type.ArrowUtilsTest.TestFlattenUnresolvableDictionaries— selective flatten:dictionary(int64, large_utf8)anddictionary(int64, utf8)decoded while theint32neighbour stays encoded; nested dictionary decoded; unchanged batch returned by identity; sliced batch keeps its offset.ReaderUtilsTest.TestApplyBitmapToReadBatchKeepsDictionaryEncoding— a deletion vector filters batches by slice +arrow::Concatenate; pins that the encoding survives it.DataFileIndexWriterTest.TestDictionaryEncodedIndexedColumnRoundTrip— bitmap index built from a forwarded column.ParquetFileBatchReaderTest.TestDictionaryPassthrough— on / explicitly off / option absent / file without dictionary pages.ParquetFileBatchReaderTest.TestDictionaryPassthroughSkipsFallbackToPlain— a column that falls back to plain inside one chunk is declined.ParquetFileBatchReaderTest.TestDictionaryPassthroughRequiresEveryRowGroup— first row group fully dictionary-encoded, second falls back; the whole column is declined.ParquetFormatWriterTest.TestWriteDictionaryEncodedColumn/ChangingAcrossBatches/WithNullsInDictionary/WithNullRows/WithDuplicateValues/EmptyBatch/OfUnsupportedTypeIsRejected.ParquetFormatWriterTest.TestGetEstimateLengthWithDictionaryBatches—GetEstimateLength()andReachTargetSize()still drive file rolling when batches arrive encoded.Integration:
AppendCompactionInteTest.TestAppendTableCompactionDictionaryPassthrough(Parquet + ORC) — asserts the input read types (idINT32,s/bdictionary(int32, utf8)on Parquet,sdictionary(int64, large_utf8)on ORC), then the full rewrite, then a predicate read through the bitmap index.AppendCompactionInteTest.TestAppendTableCompactionDictionaryPassthroughDisabled— the kill switch produces the same table.Benchmarks (
benchmark/parquet_format_benchmark.cpp):BM_ParquetWrite_DictionaryStringIntoStringSchema— the shape a rewrite produces (plainSTRINGwrite schema, dictionary-encoded batch), on the same10 / 1000 / kRowsPerFileaxis as itsBM_ParquetWrite_Stringbaseline.BM_ParquetRead_DictionaryPassthrough— the same axis with the option on and off at each cardinality; the high-cardinality point is where the gate declines and the two runs should measure the same work.API and Format
No public API under
include/changed.ArrowUtils(internal,PAIMON_EXPORT) gainsIsParquetDictionaryValueType,ResolveParquetDictionaryStructTypeandFlattenUnresolvableDictionaries; two privateAppendOnlyFileStoreWritehelpers changed signature.No storage format or protocol change. Output files remain standard Parquet — a forwarded dictionary is written through Arrow's ordinary dictionary path, and a file written with the option on is readable by any reader.
One new table option,
parquet.read.enable-dictionary-passthrough, defaultfalse. The append compaction rewrite turns it on for itself when eligible and forces it off when not; setting it tofalseon the table disables the optimization.Documentation
Yes —
docs/source/user_guide/compaction.rstgains a "Dictionary Passthrough" section covering the scope (append-only only), per-file eligibility, the three gating conditions, file-index behaviour, the plain-fallback trade-off and the kill switch.Generative AI tooling
Generated-by: Claude Code (Claude Opus 5)
🤖 Generated with Claude Code