JingsongLi commented on PR #8827:
URL: https://github.com/apache/paimon/pull/8827#issuecomment-5066373687

   # Adaptive Cross-File Inference for Variant Shredding
   
   ## 1. Summary
   
   Paimon currently infers a Variant shredding schema independently for every 
data file. Each file
   buffers up to `variant.shredding.maxInferBufferRow` rows, infers its 
physical schema, flushes the
   buffer, and then continues writing. This approach is robust to schema 
changes, but it repeats the
   same inference work for adjacent files and may produce unnecessary 
physical-schema churn.
   
   This document proposes an adaptive inference mode with three properties:
   
   1. **Previous-file prior:** retain bounded inference evidence and the 
selected physical schema from
      the preceding file in the same rolling writer.
   2. **Small-sample correction:** inspect a smaller prefix of the next file 
before finalizing its
      physical schema.
   3. **Hysteresis:** use different thresholds for retaining an existing 
shredded path and admitting a
      new path, reducing oscillation near the cardinality threshold.
   
   The prior is an optimization hint, not a correctness dependency. Every file 
remains self-describing,
   and values that do not fit a selected typed layout continue to be stored in 
the Variant `value`
   field according to the Parquet Variant shredding model.
   
   The state is intentionally scoped to one `RollingFileWriter`. It is not 
shared across tasks,
   processes, partitions, buckets, or writer restarts, and it is not persisted 
in table metadata.
   
   ## 2. Motivation
   
   Independent per-file inference has three costs.
   
   ### 2.1 Repeated buffering
   
   For a stable input stream, adjacent files often infer almost identical 
schemas. Buffering the same
   large prefix for every file adds memory pressure and delays creation of the 
underlying format
   writer. It can also cause a file to exceed its target size because rolling 
cannot be evaluated until
   the inferred plan has been finalized.
   
   ### 2.2 Physical-schema churn
   
   A field whose observed cardinality is close to
   `variant.shredding.minFieldCardinalityRatio` may be included in one file and 
excluded from the next.
   The data remains readable, but frequent layout changes reduce predictability 
and can make projected
   reads and data skipping less effective.
   
   ### 2.3 Weak inference for small files
   
   Small files may close before enough rows have been observed. A bounded prior 
supplies evidence from
   the immediately preceding stream segment without requiring table-wide 
coordination.
   
   ## 3. Goals
   
   - Reduce inference buffering after the first file in a rolling writer.
   - Preserve the ability to react to new fields and type changes at file 
boundaries.
   - Reduce repeated admission and eviction of fields near the selection 
threshold.
   - Preserve file-level correctness and independent readability.
   - Keep memory usage bounded by configured schema width and a fixed effective 
prior size.
   - Make the result deterministic for the same prior and current sample.
   - Avoid cross-writer synchronization and persistent table metadata in the 
first version.
   - Preserve the existing configured-schema behavior.
   
   ## 4. Non-Goals
   
   - Producing one globally stable shredding schema for an entire table.
   - Learning from files written by other tasks or writers.
   - Persisting inference state across checkpoints, retries, or process 
restarts.
   - Selecting fields from query history.
   - Changing the Variant encoding or Parquet shredding specification.
   - Changing the current strict type-merging policy in the first 
implementation.
   - Reconstructing prior statistics by reading the previous file footer.
   
   Query-aware pinned paths and table-level schema pinning are complementary 
future features.
   
   ## 5. Current Behavior
   
   `InferShreddingWritePlanWriter` buffers the prefix of each file and invokes
   `ShreddingWritePlanFactory.createWritePlan` once the row limit is reached or 
the writer closes.
   `VariantShreddingWritePlanFactory` creates a new 
`InferVariantShreddingSchema` and derives a
   physical row type only from those buffered rows.
   
   `RowDataRollingFileWriter` creates a new file-writer context for every 
rolled file. That operation
   also creates a new format writer factory and therefore a new 
`VariantShreddingWritePlanFactory`.
   No inference information survives the file boundary.
   
   The current selection limits are:
   
   - cold sample limit: 4,096 rows by default;
   - minimum field cardinality ratio: 0.10;
   - maximum inferred schema width: 300;
   - maximum inference depth: 50.
   
   ## 6. Design Overview
   
   Adaptive inference introduces one in-memory 
`VariantShreddingInferenceSession` for each sequential
   rolling writer.
   
   ```text
   File 1 prefix (cold sample)
           |
           v
     analyze + select plan
           |
           +----> bounded evidence + selected plan
                                 |
   File 2 prefix (warm sample)   |
           |                     |
           +---------------------+
           |
           v
    combine evidence + apply hysteresis + correct types
           |
           +----> File 2 plan and updated bounded prior
   ```
   
   The first file uses the existing cold-sample limit. Later files use a 
smaller warm-sample limit.
   For every warm file, the session:
   
   1. analyzes the current prefix;
   2. combines current field-presence evidence with bounded prior evidence;
   3. applies admission and retention thresholds;
   4. validates the previous typed layout against current observations;
   5. applies the existing depth and width limits;
   6. creates the per-file write plan; and
   7. replaces the session state with a bounded posterior for the next file.
   
   ## 7. State Scope and Lifecycle
   
   ### 7.1 Scope
   
   The session belongs to a single `RollingFileWriter` instance. In normal 
Paimon write paths, that
   also limits it to one logical stream such as a partition/bucket/level writer.
   
   The session must not be stored in:
   
   - the shared `FileFormat`, which is required to be thread-safe;
   - static state;
   - a table-wide cache; or
   - a cache keyed only by table or column name.
   
   There is no globally meaningful "previous file" when multiple tasks write 
concurrently.
   
   ### 7.2 Lifecycle
   
   - The session is created with the rolling writer.
   - The first inferred plan initializes the session.
   - A subsequent file reads and updates the session only from the writer 
thread.
   - The rolling writer opens the next file only after the current file closes 
successfully.
   - If writing or closing fails, the rolling writer aborts and discards the 
entire session.
   - A retry creates a new session and starts with cold inference.
   - A configured shredding schema takes precedence and bypasses adaptive 
inference.
   
   No extra commit protocol is required because the state is ephemeral and 
cannot be observed by
   readers.
   
   ## 8. Inference Evidence
   
   The implementation should separate **analysis** from **schema 
materialization**. The current
   `InferVariantShreddingSchema` combines both operations, which makes it 
difficult to reuse bounded
   evidence without carrying a partially materialized `RowType`.
   
   A conceptual model is:
   
   ```java
   final class VariantShreddingInferenceState {
       private final Map<List<Integer>, VariantColumnEvidence> columns;
       private final RowType previousPhysicalRowType;
       private final long completedFileCount;
   }
   
   final class VariantColumnEvidence {
       private final double effectiveRootValueCount;
       private final Map<VariantFieldPath, FieldEvidence> fields;
   }
   
   final class FieldEvidence {
       private final double effectivePresenceCount;
       private final DataType previousSelectedType;
       private final boolean previouslySelected;
   }
   ```
   
   The actual implementation may use compact tree nodes instead of path-keyed 
maps. Field paths and
   types must use deterministic equality and ordering.
   
   For the first version, the reusable evidence only needs:
   
   - the effective number of non-null root Variant values;
   - the effective presence count of each object field;
   - whether the path was selected in the preceding plan; and
   - the type selected in the preceding plan.
   
   The current sample still supplies exact type observations. Persisting a 
complete type histogram or
   full-file fallback statistics can be added later.
   
   ### 8.1 Cardinality denominator
   
   To preserve current behavior, a nested field's cardinality remains relative 
to the number of
   non-null values of its root Variant column, rather than only to the number 
of occurrences of its
   immediate parent.
   
   Each Variant column maintains its own denominator. If the current prefix 
contains no non-null value
   for a Variant column, that column's prior is retained without decay because 
the sample provides no
   evidence about its shape.
   
   ## 9. Combining Prior and Current Evidence
   
   Raw counts from the cold sample must not dominate all future files. The 
prior is therefore converted
   to a bounded effective sample.
   
   Let:
   
   - `P` be the previous bounded evidence;
   - `C` be evidence from the current warm prefix; and
   - `W` be the warm-sample row limit.
   
   For each Variant column:
   
   1. scale `P` so that its effective root count is at most `W`;
   2. add the current counts from `C`;
   3. compute field presence ratios from the combined counts; and
   4. scale the combined evidence back to at most `W` before storing it as the 
next prior.
   
   Conceptually:
   
   ```text
   boundedPrior = scaleToAtMost(previousEvidence, W)
   posterior    = boundedPrior + currentEvidence
   nextPrior    = scaleToAtMost(posterior, W)
   ```
   
   With a full warm sample and the default equal cap, the previous evidence and 
the current file have
   approximately equal influence. Repeated absence naturally decays a field's 
rate: 100%, 50%, 25%,
   12.5%, and so on. The prior can therefore stabilize a path without making it 
permanent.
   
   The first file may analyze up to the existing 4,096-row limit, but its 
evidence is scaled down before
   it becomes a prior.
   
   ## 10. Hysteresis and Field Selection
   
   Let:
   
   - `A` be the existing admission threshold,
     `variant.shredding.minFieldCardinalityRatio`; and
   - `R` be the retention threshold, with `0 <= R < A`.
   
   The proposed default is:
   
   ```text
   A = 0.10
   R = 0.05
   ```
   
   A field is eligible when:
   
   ```text
   previously selected: combinedPresenceRatio >= R
   not previously selected: combinedPresenceRatio >= A
   ```
   
   This gap prevents a field near 10% from alternating between shredded and 
unshredded layouts on
   every file.
   
   ### 10.1 Width-budget ordering
   
   Hysteresis must not allow stale fields to permanently occupy a full width 
budget. Eligible fields
   are therefore ranked by:
   
   1. combined presence ratio, descending;
   2. previously selected status, selected first when ratios are equal; and
   3. complete field path, lexicographically ascending.
   
   Parent nodes required to reach a selected child are included before the 
child. Existing width and
   depth accounting remains authoritative. If two schemas consume the same 
budget, the deterministic
   ordering produces the same result for the same evidence.
   
   ## 11. Type Selection and Correction
   
   Field-membership hysteresis must not make type changes unnecessarily sticky.
   
   For a path selected in the previous plan:
   
   1. If the current sample has no value for the path, retain the previous 
selected type.
   2. If all current observations are compatible with the previous type, retain 
it and apply existing
      decimal/integer widening rules when required.
   3. If any current observation is incompatible, discard the previous type 
hint and infer the type
      from the current sample using the existing strict merge rules.
   
   For a newly admitted path, infer its type from the current sample using the 
existing rules.
   
   This policy deliberately applies hysteresis to **field membership**, but 
immediate correction to
   **incompatible types**. It avoids carrying a stale typed layout across 
several files while keeping
   the first implementation behaviorally close to current Paimon.
   
   Values after the sampled prefix may still fail to fit the selected type. 
They remain lossless in the
   Variant `value` field. This is a correctness fallback, but a high fallback 
rate can reduce
   compression, projection, and data-skipping benefits.
   
   A follow-up design may select the most common compatible type with a 
configurable fit threshold,
   similar to other Variant implementations. That policy change should be 
evaluated separately.
   
   ## 12. Buffer Limits
   
   The adaptive mode uses:
   
   - the existing `variant.shredding.maxInferBufferRow` for the first file; and
   - a smaller warm-file row limit for subsequent files.
   
   Inference should also stop at a byte limit. A row-only limit is insufficient 
when a small number of
   Variant values contain large strings, objects, arrays, or binary values.
   
   The writer should finalize its plan when either limit is reached:
   
   ```text
   bufferedRows >= rowLimit || estimatedBufferedBytes >= byteLimit
   ```
   
   The byte estimator does not need to model Java object overhead exactly. It 
should conservatively
   include Variant binary/value bytes and fixed row overhead, be deterministic, 
and avoid traversing
   the same Variant tree twice.
   
   ## 13. Writer Integration
   
   The smallest integration is to create the format writer factory once per
   `RowDataRollingFileWriter`, then reuse that factory for the sequential files 
produced by the rolling
   writer.
   
   Today, `RowDataRollingFileWriter` calls:
   
   ```text
   fileFormat.createWriterFactory(writeSchema)
   ```
   
   inside the per-file supplier. In adaptive mode, it should happen once when 
the supplier is built.
   For Parquet, the resulting `ShreddingWritePlanWriterFactory` contains one
   `VariantShreddingWritePlanFactory`, which in turn owns the adaptive session.
   
   Per-file components must remain per-file:
   
   - output streams;
   - `RowDataFileWriter`;
   - statistics producers and collectors;
   - file-index writers;
   - sidecar writers; and
   - writer metadata.
   
   Only the reusable `FormatWriterFactory` and its bounded inference session 
cross file boundaries.
   
   `ShreddingWritePlanFactory.inferBufferRowCount()` may return the cold or 
warm limit based on whether
   the session has a prior. `createWritePlan(sampleRows)` analyzes the current 
sample, materializes the
   plan, and advances the session.
   
   The factory is used sequentially by one rolling writer. It is not advertised 
as thread-safe and
   must not be shared between rolling writers.
   
   ## 14. Configuration
   
   The following options are proposed:
   
   | Option | Default | Description |
   | --- | ---: | --- |
   | `variant.shredding.inferenceMode` | `PER_FILE` | `PER_FILE` preserves 
current behavior; `ADAPTIVE` enables rolling-writer-scoped prior reuse. |
   | `variant.shredding.adaptive.maxInferBufferRow` | `256` | Maximum prefix 
rows for files after the first file in the session. |
   | `variant.shredding.adaptive.retentionRatio` | `0.05` | Minimum combined 
presence ratio for a path selected in the previous plan. Must not exceed 
`variant.shredding.minFieldCardinalityRatio`. |
   | `variant.shredding.maxInferBufferBytes` | `64 MB` | Maximum estimated 
bytes retained for inference in either mode. |
   
   The existing options continue to define:
   
   - cold-file row limit;
   - admission threshold;
   - maximum schema width; and
   - maximum schema depth.
   
   The initial release should keep `PER_FILE` as the default. After production 
evidence demonstrates
   stable write and read improvements, changing the default can be considered 
independently.
   
   ## 15. Correctness and Compatibility
   
   ### 15.1 Read correctness
   
   Each file persists its own physical Variant shredding schema. Readers 
already support different
   physical layouts across files. The adaptive session affects only how a file 
chooses that layout.
   
   Values incompatible with a typed field are encoded in `value`, so prior 
reuse does not discard or
   coerce user data.
   
   ### 15.2 Data skipping
   
   A stale type may increase non-null `value` occurrences. Typed-column 
statistics alone cannot
   describe those fallback values. The adaptive design therefore uses the 
current sample to correct
   incompatible prior types immediately.
   
   Future full-file fallback metrics should be used to detect paths whose typed 
layout is not
   beneficial even when the prefix appeared compatible.
   
   ### 15.3 Backward compatibility
   
   - No file-format or table-metadata change is required.
   - Existing files require no rewrite.
   - Configured shredding schemas are unchanged.
   - `PER_FILE` mode preserves the current inference lifecycle.
   - Adaptive and non-adaptive files may coexist in the same table.
   
   ## 16. Failure, Retry, and Concurrency Semantics
   
   - Session mutations occur only in the rolling writer's thread.
   - The next file cannot consume the updated prior until the current file has 
closed.
   - If the current file fails, the writer aborts and no subsequent file 
observes its prior.
   - A task retry starts cold; its physical layouts may differ from the failed 
attempt, which is valid
     because layouts are file-local.
   - Concurrent writers never share inference sessions.
   - Schema evolution creates a new rolling writer/session. If an 
implementation can reuse a writer
     across logical schema changes, it must reset the session when the 
logical-row-type fingerprint
     changes.
   
   ## 17. Observability
   
   Adaptive mode should expose metrics or debug counters for:
   
   - cold versus warm inference count;
   - sampled rows and estimated bytes;
   - inference time;
   - number of admitted, retained, evicted, and type-corrected paths;
   - inferred physical-schema width and depth;
   - physical-schema fingerprint changes between adjacent files; and
   - Variant values or bytes falling back from `typed_value` to `value`, if the 
writer can report them.
   
   The schema fingerprint should be deterministic and suitable for measuring 
churn, but it does not
   need to be persisted in table metadata.
   
   ## 18. Test Plan
   
   ### 18.1 Unit tests
   
   Add focused tests for:
   
   - first-file cold inference and later-file warm limits;
   - stable adjacent files retaining the same plan;
   - a new hot field being admitted;
   - an old field decaying below the retention threshold;
   - a field oscillating between the admission and retention thresholds;
   - a full width budget replacing a stale low-frequency field with a hotter 
new field;
   - immediate correction of an incompatible previous type;
   - numeric widening compatible with the previous type;
   - no current non-null Variant values retaining the prior without decay;
   - independent state for multiple Variant columns;
   - nested fields, arrays, nulls, and depth limits;
   - deterministic ordering for equal-frequency paths; and
   - state reset after creating a new session.
   
   ### 18.2 Writer integration tests
   
   Use a small target file size to force one rolling writer to create multiple 
Parquet files, then
   verify:
   
   - the first and warm-file sample limits;
   - the physical schema stored in each file;
   - logical read equality across mixed physical layouts;
   - lossless fallback for values that appear after the sampled prefix;
   - behavior when a file fails or is aborted; and
   - no state leakage between two rolling writers.
   
   ### 18.3 Benchmarks
   
   Compare:
   
   1. current per-file inference with a 4,096-row sample;
   2. direct previous-plan reuse without correction;
   3. adaptive prior plus a 256-row correction sample; and
   4. configured/pinned schema.
   
   Use the following workloads:
   
   - stationary schemas;
   - sparse Zipf-distributed keys;
   - fields near the cardinality threshold;
   - abrupt schema shifts at file boundaries;
   - alternating types;
   - large Variant values;
   - partition- or sensor-sorted streams; and
   - compaction output mixing several input schemas.
   
   Measure:
   
   - write throughput and time to initialize each physical writer;
   - peak inference-buffer bytes;
   - schema-fingerprint churn;
   - shredded-path coverage and fallback bytes;
   - file size and Parquet leaf-column count;
   - projected read bytes and latency; and
   - predicate/data-skipping effectiveness.
   
   ## 19. Rollout Plan
   
   1. Refactor Variant inference into analysis, evidence combination, and 
schema materialization
      without changing current behavior.
   2. Add the byte cap to per-file inference.
   3. Add the rolling-writer-scoped session behind `ADAPTIVE` mode.
   4. Add unit, integration, and benchmark coverage.
   5. Run shadow metrics that compare the adaptive plan with the current 
per-file plan without changing
      the written schema.
   6. Enable adaptive mode in selected workloads and evaluate schema churn, 
fallback rate, memory, and
      read performance.
   7. Consider changing the default only after results are stable across 
stationary and schema-shift
      workloads.
   
   ## 20. Alternatives Considered
   
   ### 20.1 Reuse the previous physical schema without sampling
   
   This minimizes buffering but cannot detect new hot paths or boundary-aligned 
schema changes. It may
   increase fallback values and weaken data skipping. It is not recommended.
   
   ### 20.2 Use a smaller independent sample for every file
   
   This reduces buffering but loses evidence for stable sparse fields and 
increases threshold noise.
   It does not address physical-schema churn.
   
   ### 20.3 Read the previous file footer
   
   The footer exposes the selected physical schema but not the observations 
that caused the selection,
   including frequency and rejected alternatives. The writer already has richer 
information in memory,
   so footer I/O adds latency without improving the prior.
   
   ### 20.4 Persist one table-wide learned schema
   
   This can provide stronger stability but requires coordination, evolution 
rules, ownership, and
   metadata compatibility. It is better treated as explicit schema pinning 
rather than an implicit
   extension of per-file inference.
   
   ### 20.5 Aggregate compaction input schemas
   
   This is useful for compaction because there is no single representative 
previous input file.
   Compaction-aware aggregation is compatible with this design but should be a 
separate extension.
   
   ## 21. Open Questions
   
   - Should the warm-sample default be 128, 256, or derived from the target 
file size?
   - Is a 0.05 absolute retention threshold preferable to a fixed fraction of 
the admission threshold?
   - Can the Variant writer report full-file fallback counts cheaply enough to 
feed the next prior?
   - Should compaction initialize its session from aggregated input-file 
metadata?
   - Which paths, if any, should users be able to pin ahead of inferred paths?
   - After the adaptive lifecycle is proven, should type selection move from 
strict consistency to a
     minimum fit ratio with minority values stored in `value`?
   
   ## 22. References
   
   - [Parquet Variant 
Shredding](https://github.com/apache/parquet-format/blob/master/VariantShredding.md)
   - [Databricks Variant 
Shredding](https://docs.databricks.com/aws/en/tables/features/variant-shredding)
   - [Delta Lake Variant Shredding 
Protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#variant-shredding)
   - [Apache Spark Variant Shredding 
Inference](https://github.com/apache/spark/pull/52406)
   - [Apache Iceberg Variant Shredding 
Analyzer](https://github.com/apache/iceberg/blob/main/parquet/src/main/java/org/apache/iceberg/parquet/VariantShreddingAnalyzer.java)
   - [DuckDB Automatic Variant 
Shredding](https://github.com/duckdb/duckdb/pull/19336)
   


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