feat: keep column groups through compaction and add rewrite_columns - #9291
dshepelev15 wants to merge 2 commits into
Conversation
Compaction re-encodes every column of the fragments it rewrites, so migrating a table with a few multi-KB embedding columns to a newer file version rewrites the whole table for the sake of its narrow columns, and compaction folds any per-column file layout back into one wide file, which makes later single-column updates rewrite every column again. `CompactionOptions.column_groups` (config key `lance.compaction.column_groups`) writes each listed group of top-level columns to its own data file per compacted fragment and the remaining columns to one shared file. One scan feeds one writer per group with the same planned row counts, so the files of a fragment stay row-aligned; binary copy is disabled and `max_bytes_per_file` ignored while groups are set. `Dataset::rewrite_columns` / `FileFragment::rewrite_columns` rewrite only the named columns of every fragment into a new data file in the requested V2 version and tombstone them in the files they came from. The other columns' files are neither read nor written; rows, fragment ids, row addresses and indices are unchanged. Fragments already in the requested layout are skipped, and the fragment-level call returns metadata for a single distributed `Update` commit in `RewriteColumns` mode.
Fold the two internal errors on misaligned column group output into one, hand the forwarder its projections by value instead of cloning them, and look up an unknown rewrite column with find instead of a loop.
|
I did similar thing before: #8614 This change been postponed because I think it's better to refactor our compaction logic so that make lance to produce stats instead of a black box. |
There was a problem hiding this comment.
✅ Gate recommendation: approve.
The earlier physical-layout PR and the statistics-first concern do not change the recommendation. Producing layout statistics would improve policy selection, but it does not replace the executor needed to rewrite chosen groups. This PR supplies that row-aligned execution through existing compaction jobs and table configuration, while rewrite_columns avoids rewriting wide columns; its row, index, overlay, and stable-row-ID contracts remain preserved.
Please mark this PR with the breaking-change label.
| def rewrite_columns( | ||
| self, | ||
| columns: List[str], | ||
| *, | ||
| data_storage_version: Optional[str] = None, | ||
| ): |
There was a problem hiding this comment.
suggestion(blocking): instead of adding a new API, I think we should find a way to fold this into the existing compaction operation. That way we aren't adding yet another maintenance operation a user has to call. Rewrite_columns can just be a particular runtime configuration you pass, if you only want to do those jobs. #8614 (comment)
What
CompactionOptions.column_groups(Pythoncompact_files(column_groups=[["emb"], ["cap", "tags"]]), dataset config keylance.compaction.column_groups="emb;cap,tags"): each listed group of top-level columns is written to its own data file per compacted fragment; every column not listed goes to one shared file. A single scan feeds one writer per group with the same planned row counts, so the files of a fragment stay row-aligned. Binary copy is disabled while groups are set (force_binary_copyreports NotSupported) andmax_bytes_per_fileis ignored so all groups split at the same rows. Unknown columns fail at planning, before tasks are distributed. The Java JNI builds its options throughfrom_dataset_config, so it needs no change and picks the groups up from the table config.Dataset::rewrite_columns(columns, data_storage_version)/FileFragment::rewrite_columns(PythonLanceDataset.rewrite_columns,LanceFragment.rewrite_columns): rewrite only the named top-level columns of every fragment into one new data file in the requested V2 version and tombstone them in the files they came from; a file left holding only tombstones is dropped. The other columns' files are neither read nor written, and rows, fragment ids, row addresses and indices are unchanged. Commits oneUpdateinRewriteColumnsmode with emptyfields_modified, since the values did not change. Fragments whose columns already sit alone in a file of the requested version are skipped, so an interrupted rewrite can be rerun; the fragment-level call returns metadata for a single distributed commit.Why
Compaction rewrites whole fragments, so moving a table to a newer data file version through compaction re-encodes every column, including the multi-KB embedding columns that dominate its size. It also folds any per-column file layout (from
merge_insertinrewrite_columnsmode oradd_columns) back into one wide file, which makes the next single-column update rewrite every column again. Together the two options let a table keep its wide columns in their own files across compactions and migrate its narrow columns without touching them.Notes
rewrite_columnsrejects blob columns for the same reason the merge-insertRewriteColumnspath does: the stored descriptor form differs from the logical form the writer expects. Nested paths are not accepted incolumn_groups.versions::validate_write_schemafactors out the blob checkswrite_fragmentsalready ran, andcreate_seed_writersbecomespub(super)so the grouped writer can seed indices too.dataset::optimize(column groups across compaction modes and target versions, config parsing, validation) anddataset::rewrite_columns(migration and layout, index coverage kept, bad input, compaction preserving the layout); Pythontest_optimize.pyandtest_schema_evolution.py, including the per-fragment commit path.