--- title: "Compaction and cleanup" sidebarTitle: "Compaction" description: "Merge small files, prune old versions, and fold new rows into existing indexes." icon: broom --- Every write commits a new table version, and versions accumulate: small files from frequent appends, superseded files from updates and deletes, and rows added since an index was built. `optimize()` addresses all three. It is modelled after `VACUUM` in PostgreSQL — routine housekeeping that a table needs periodically rather than a repair for something broken. ## What it does {#what-it-does} ```python table.optimize() ``` Three operations, in one call: | | | | --- | --- | | **Compaction** | merges small files into larger ones | | **Prune** | removes versions older than the retention window | | **Index** | folds newly added rows into existing indexes | The third is the one people miss. An [index](/indexing) does not automatically cover rows appended after it was built — those rows are still searchable, but by scanning rather than through the index. Running `optimize()` is what brings them under it. ## Retention {#retention} ```python from datetime import timedelta table.optimize(cleanup_older_than=timedelta(days=30)) ``` `cleanup_older_than` defaults to **7 days**. Versions older than that are removed, along with the files only they referenced. The latest version is never removed, whatever you pass — `timedelta(0)` prunes everything except it. Pruning is what makes [time travel](/management/table#versioning) finite. A version outside the retention window is gone, and a tag or branch pointing at it no longer resolves. If you rely on reading old versions, set the window to cover the range you need before running this, not after. ## Files from failed transactions {#unverified} A write that fails partway can leave files that look like an operation still in progress. `optimize()` will not remove them until they are at least seven days old, because it cannot tell them apart from a live write. ```python table.optimize(delete_unverified=True) ``` `delete_unverified=True` removes them regardless of age. Only set it when you can guarantee **no other process is writing to the table** — otherwise it can delete files belonging to a write that is genuinely in progress, and corrupt the table. ## Retraining indexes {#retrain} ```python table.optimize(retrain=True) ``` Folding new rows into an index preserves the structure the index was trained on. When the data has shifted enough that the structure itself is a poor fit — a vector index whose clusters no longer reflect the distribution — `retrain=True` rebuilds it rather than extending it. More expensive than a normal optimize, and worth it after a large ingest that changed the shape of the data rather than just its volume. ## When to run it {#when} There is no background compactor: `optimize()` runs when you call it. Frequent small appends are what make it necessary, because each one commits files. A table written in a few large batches needs it rarely; one taking continuous small writes needs it on a schedule. Compaction rewrites files, which conflicts with a concurrent [computed-column refresh](/feature-engineering/add-computed-column) or [view refresh](/feature-engineering/materialized-views) planned against the version it replaces. Sequence them rather than running both at once. ## Performance {#performance} Two things accumulate on a long-lived table as more and more data gets added to it: - **Many small fragments build up as you write**, slowing down queries that have to scan across more files. **Compaction** merges them back into larger fragments. - **Old versions build up as the table changes**, growing disk usage beyond the live data size (LanceDB retains them for time-travel and rollback). **Cleanup** prunes versions older than a retention window. In OSS, you run them yourself via `optimize()`, which bundles both into a single call: ```python Python icon="python" from datetime import timedelta table.optimize() # 7-day default retention table.optimize(cleanup_older_than=timedelta(days=1)) # reclaim space sooner ``` The best practice is to run `optimize()` after large writes or on a schedule. It also bundles incremental index updates — see [Reindexing](/indexing/reindexing) for the breakdown. Updates also move rows out of the vector index, so they remain searchable but unindexed — rebuild the index after large update batches. LanceDB Enterprise handles both compaction and cleanup automatically.