Replies: 1 comment 1 reply
|
The metadata amplification you’ve measured is a workload we want the manifest redesign to address, including the impact on readers of unchanged columns. We already have an ongoing design discussion in #7499, with the fragment metadata tree format taking shape in #9060. It uses buffered mutations and immutable subtree reuse to avoid rewriting the full fragment list for small updates, including incremental column backfills. I’d like to bring your use case into that discussion and see how we can meet these requirements within the existing design. In particular, your unchanged-column reader workload is valuable: the tree is organized by fragment ID, so column-level isolation is something we should examine explicitly. Would you join #7499 and share your reproducer and the read/write behavior you want to achieve? We can use those cases to evaluate the tree design together and identify where it needs changes. That should help us converge on one metadata architecture that handles both fragment growth and wide-table evolution. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
The Lance manifest is a full snapshot: it materializes the schema plus, for every fragment, the data files of every column. Its size and rewrite cost therefore scale with
O(columns × fragments), and every commit rewrites all of it. On wide tables (thousands of columns) with continuous schema evolution and ingestion, this couples unrelated columns: changing one column rewrites, re-reads, and invalidates the metadata of all of them.We propose splitting the manifest into:
The goal is to shrink the blast radius of a column change from the whole table to a single group. This is a metadata-format change; it does not change row semantics, deletion semantics, time travel, or index semantics.
Motivation, with measurements
All numbers below are from a local benchmark (pylance 11.0.0, local NVMe, Linux), dataset
C=500columns ×F=20,000fragments × 2,048 rows/fragment, 20 consecutiveadd_columnscommits, one new Int32 column each. Parameters are recorded by the writer process (repro scripts available on request; we will attach them as a gist). We considerC=500a modeling parameter: the per-column metadata cost is linear (74 B / column / fragment), so costs extrapolate mechanically, but we have not yet run a 10k-column dataset.Write path (per one-column add):
Read path (a separate reader process continuously querying one column that never changes, with a 1 s consistency check):
refresh_ms:checkout_latestcost when the version changedThe baseline-erosion point is the one we want to highlight: the spike after a commit is transient, but every added column permanently raises the steady-state cost of every reader, including readers whose projections never touch the new column. (Mechanism: the manifest cache key is
(version, e_tag)—rust/lance/src/session/caches.rs— with no column dimension, so any commit invalidates every reader's manifest, and the decoded metadata that queries plan against grows with every add.)The manifest-size flip side is also cheap to observe today:
Dataset::versions()already knows each version's on-disk manifest size but drops it (a patch addingVersion.manifest_sizeis ready and will follow);WriteResult.sizeis computed on every commit and dropped (#9087 logs it).Proposed design (sketch)
ColumnManifestRefper group (group id, field ids, path, optional base path). Grouped fields' files are removed fromFragment::files._column_manifests/group_NNNN/{uuid}.manifestand updates the reference in the root. Unchanged groups are not read, rewritten, or copied — and because their identity is their path (not the table version), a reader's cache entry for an unchanged group survives unrelated commits. (We deliberately say path-addressed immutable objects, not content-addressed: v1 does not deduplicate identical content across groups/versions.)add_columnstouches: the new column's data files, the target group's manifest, and a small root delta — not the full fragment list. Transactions carryschema delta + group refs delta.Expected effect at the benchmark's scale: per-commit metadata write ~99 MB → a few MB; version-bump re-read ~50 MiB → root + one group (~2 MB); baseline erosion for untouched groups structurally eliminated. The root schema remains
O(C)in v1, so an add still rewrites the root and its schema — we do not claim O(1).Relationship to existing mechanisms
base_pathsalready lets data files live outside the dataset; group manifests can use it per group.IndexSectionis precedent for splitting cold metadata out of the hot manifest; version aux data is precedent for "not loaded by default".Compatibility strategy
One new feature flag:
FLAG_UNSTABLE_COLUMN_GROUP_MANIFEST = 1 << 9, env-gated while experimental (LANCE_ENABLE_UNSTABLE_COLUMN_GROUP_MANIFEST), always understood in debug builds for tests. Datasets that don't use groups are byte-identical to today.Two compatibility notes that need maintainer eyes:
1 << 8to1 << 10. The bit at1 << 8is the reserved mixed-data-file-versions capability; it must remain refused, so it is explicitly excluded from the supported set rather than being covered by the boundary. Any bit between the old and new boundary must be either supported or explicitly reserved — this is the same pattern used when the covered-index-metadata flag reclaimed bit 7.Prototype status
To make the discussion concrete, we have a vertical slice (not proposed for merge as-is):
docs/src/format/table/column_group_manifest.md(branchcg-manifest/06-format-spec)cg-manifest/05-flag-and-proto(protobuf messagesColumnManifestRef/ColumnManifest/ColumnFragmentData,Manifestfield 22)split_into_column_groups()+ immutable group-manifest file write/read (cg-manifest/07)cg-manifest/08)Separately, small observability PRs that make the RFC numbers reproducible in-repo (submitted as #9087 and #9091):
WriteResult.sizeis currently discarded)Version.manifest_sizeinlist_versions()manifest_commitbench parameterized byNUM_COLUMNS, reporting manifest bytes per commitBenchmark scripts and raw data (Python, reader/writer in separate processes — same-process read+write skews results badly): links in the repo issue tracker / attached.
Open questions
O(C). Is a per-group schema split worth a v2?What we're asking
Happy to restructure, rescope, or drop parts of this based on feedback.
All reactions