← Delta Optimization track

Databricks · Delta Optimization · Lesson 04

OPTIMIZE &
compaction.

Busy tables accumulate thousands of small files. OPTIMIZE bin-packs them into fewer, right-sized files — same data, same results, far faster reads. It's idempotent, safe to run on a live table, and on managed tables the platform runs it for you.

Bin-packing idempotent Balances by file size Readers not interrupted OPTIMIZE FULL DBR 16.0+ Verified Jun 2026 docs

What it is

OPTIMIZE is the command that takes the many small files a table has accumulated and rewrites them into fewer, right-sized files. That merging is called bin-packing — packing lots of little items into a smaller number of evenly-filled bins (here, the bins are data files of a healthy size). Nothing about your data changes — same rows, same values, same query results — only the physical file layout on storage gets tidier.

Why do small files pile up? Every streaming micro-batch, every MERGE/UPDATE/DELETE, every tiny append writes new Parquet files. A busy table can grow thousands of tiny files in a day. Each file carries fixed overhead — list it, open it, read its footer and stats, schedule a task for it — so thousands of tiny files make every read slow and metadata-heavy. OPTIMIZE is the broom that sweeps them back into a small number of well-sized files.

Analogy
It's like consolidating a junk drawer of half-empty envelopes into a few full folders. Same papers inside, far fewer things to pick up and flip through. Bin-packing balances folders by how thick they are (file size in storage), not by how many papers each holds (row count).

Real-world use case
A streaming events table ingests every few seconds and by noon has 40,000 files averaging 2 MB. A nightly OPTIMIZE compacts them to a few hundred ~256 MB files; the morning dashboards go from minutes to seconds. (On a UC managed table you'd let predictive optimization run this for you.)

Why it matters

The one rule to remember OPTIMIZE fixes the small-file problem after the fact; it's idempotent, safe to run on a live table (snapshot isolation — readers and streams aren't interrupted), and on modern UC managed tables you let predictive optimization run it automatically.

See it work — bin-packing simulator

Below is a table stored as 32 small files. Click Run OPTIMIZE and watch the engine bin-pack them into a handful of right-sized files. The numFiles drops, the average file size jumps, and the read gets cheaper — same data throughout.

Bin-packing before / after

A 32-file Delta table after a day of streaming micro-batches (~8 MB each).

OPTIMIZE main.delta_opt_demo.events;
Files on disk — before OPTIMIZE (many small files)
small file (~8 MB) compacted file (right-sized)
32
numFiles
8 MB
avg file size
256 MB
total size (unchanged)
A table with 32 tiny files: every read must list, open and schedule a task for all 32. Click Run OPTIMIZE to bin-pack them.

Simplified illustration. Real bin-packing balances files by their size in storage toward a target (autotuned ~256 MB→1 GB or an explicit delta.targetFileSize); total bytes are roughly preserved and results never change.

See it work — idempotency demo

Bin-packing is idempotent: the first OPTIMIZE compacts the files; a second, immediate run finds nothing left to merge and is a no-op (zero files rewritten). Contrast with a non-idempotent operation like OPTIMIZE … ZORDER BY, which can rewrite again on a re-run. Click repeatedly and watch the run-log.

Idempotency: run OPTIMIZE again

Each click issues one OPTIMIZE and records what it actually did.

OPTIMIZE main.delta_opt_demo.events;
0
runs issued
0
files rewritten
0
no-op runs
Click Run OPTIMIZE again. With bin-packing, only the first run does work — every later run is a no-op.

Simplified illustration. Idempotency applies to plain bin-packing; OPTIMIZE … ZORDER BY is not idempotent. Whether a real run does work depends on how fragmented the table currently is.

How it works — sub-topic by sub-topic

1 · OPTIMIZE table_name — bin-packing into fewer right-sized files

OPTIMIZE reads the table's small Parquet files and rewrites their contents into a smaller number of larger files, then commits a new version pointing at them. This is bin-packing — greedily packing rows into output files until each reaches the target size. Fewer, larger files mean less listing/opening/footer-reading/task-scheduling on every read. The cost: a one-time CPU-heavy rewrite — run it on a schedule, not after every micro-batch.

SQL-- Compact ALL small files into fewer right-sized files (bin-packing).
-- Same data, same results afterward — only the physical layout changes.
OPTIMIZE main.delta_opt_demo.events;
PySpark# DeltaTable API equivalent of `OPTIMIZE table_name`.
from delta.tables import DeltaTable
dt = DeltaTable.forName(spark, "main.delta_opt_demo.events")
dt.optimize().executeCompaction()   # bin-packing (compaction only, no Z-order)

2 · OPTIMIZE t WHERE … — limit to a partition subset

OPTIMIZE t WHERE <partition predicate> restricts compaction to the files matching the predicate — e.g. only yesterday's partitions. The WHERE must reference partition columns; it scopes which files get rewritten, not which rows. On a big partitioned table you rarely need to recompact well-sized history, so scoping to recently-written partitions makes maintenance cheap.

SQL-- Only recompact the partitions that recently fragmented (cheaper).
-- The predicate must reference PARTITION columns.
OPTIMIZE main.delta_opt_demo.events
WHERE event_date >= DATE'2026-06-25';
PySpark# DeltaTable API: scope with .where() before executeCompaction().
dt = DeltaTable.forName(spark, "main.delta_opt_demo.events")
(dt.optimize()
   .where("event_date >= '2026-06-25'")   # partition predicate — limits the rewrite
   .executeCompaction())

3 · Python / Scala DeltaTable API: optimize().executeCompaction()

The DeltaTable API mirrors the SQL command. DeltaTable.forName(spark,"t").optimize().executeCompaction() runs bin-packing; chain .where("…") to scope to a partition subset, exactly like SQL. A Scala equivalent has the same shape. (For Z-order on non-LC tables the method is .executeZOrderBy(...) instead.) Use the API when you're orchestrating maintenance in PySpark/Scala — e.g. compacting several tables in a loop.

PySpark# Compact a list of tables, scoped to recent partitions.
from delta.tables import DeltaTable
for tbl in ["main.delta_opt_demo.events", "main.delta_opt_demo.clicks"]:
    (DeltaTable.forName(spark, tbl)
       .optimize()
       .where("event_date >= '2026-06-25'")   # optional; drop for a full compaction
       .executeCompaction())
Scala// Scala equivalent — same call shape as Python.
import io.delta.tables._
val dt = DeltaTable.forName(spark, "main.delta_opt_demo.events")
dt.optimize().where("event_date >= '2026-06-25'").executeCompaction()

4 · Bin-packing is idempotent, and balances by file SIZE

Bin-packing is idempotent — run OPTIMIZE twice and the second run has no effect, because the files are already right-sized. It aims for files evenly balanced by their size in storage, not by row count (though the two usually correlate). Idempotency makes OPTIMIZE safe to schedule aggressively: a no-op re-run costs almost nothing. (This applies to bin-packing; ZORDER is not idempotent — see sub-topic 6.)

SQL-- First run compacts the small files...
OPTIMIZE main.delta_opt_demo.events;
-- ...a second, immediate run is a NO-OP (idempotent): nothing left to bin-pack.
OPTIMIZE main.delta_opt_demo.events;   -- numFiles unchanged, no files rewritten

5 · Liquid clustering vs partitioned tables

On a liquid clustering table, OPTIMIZE doesn't just compact — it groups data by the clustering keys, incrementally (rewriting only what's needed to keep the layout healthy). On a partitioned table, compaction and layout happen within each partition — OPTIMIZE never merges files across partition boundaries. So an over-partitioned table (Lesson 02) can still have small files that compaction can't fully fix.

SQL-- LIQUID CLUSTERING table: OPTIMIZE is INCREMENTAL and groups by clustering keys.
OPTIMIZE main.delta_opt_demo.events;

-- PARTITIONED table: compaction happens WITHIN each partition (never across them).
OPTIMIZE main.delta_opt_demo.events_partitioned
WHERE event_date = DATE'2026-06-25';

6 · OPTIMIZE t ZORDER BY (col) — compaction + Z-order (non-LC only)

Adding ZORDER BY (cols) makes OPTIMIZE do two jobs at once: bin-pack and colocate rows along a space-filling curve so the chosen columns get tight, non-overlapping min/max ranges (Lesson 03) — better data skipping. Only for non-liquid-clustering tables (ZORDER is incompatible with liquid clustering), and unlike compaction it is NOT idempotent — re-running can rewrite again. For new tables, prefer liquid clustering.

SQL-- Compact AND colocate by the columns you filter on most (non-LC tables only).
-- NOTE: not idempotent — schedule deliberately, not on every job.
OPTIMIZE main.delta_opt_demo.events
ZORDER BY (event_type, user_id);   -- 1-2 dominant filter cols; more = less benefit each
PySpark# DeltaTable API: Z-order uses executeZOrderBy(...), not executeCompaction().
dt = DeltaTable.forName(spark, "main.delta_opt_demo.events")
dt.optimize().executeZOrderBy("event_type", "user_id")   # non-LC; NOT idempotent

7 · OPTIMIZE table_name FULL (DBR 16.0+) — force full reclustering

For a liquid clustering table, OPTIMIZE t FULL forces a full reclustering — it rewrites all data to the current clustering keys, not just the incremental delta. Run it right after you first enable clustering or change the keys, since plain OPTIMIZE is incremental and leaves existing data laid out the old way. It rewrites the whole table, so it can take hours on a big table — a one-time operation, not routine upkeep.

SQL-- After enabling clustering or CHANGING keys, force a full recluster (DBR 16.0+).
-- Rewrites ALL data to the current keys — can be slow/expensive on big tables.
ALTER TABLE main.delta_opt_demo.events CLUSTER BY (event_type, event_date);
OPTIMIZE main.delta_opt_demo.events FULL;   -- one-time, after the key change

8 · Snapshot isolation — live tables & streams are safe

Delta readers use snapshot isolation: they read a consistent committed version. When OPTIMIZE swaps the old small files for new compacted ones, in-flight readers keep reading their snapshot and aren't interrupted. Because OPTIMIZE makes no data changes, a read before and after returns the same results. Running it on a table that is a streaming source does not affect current or future streams. That's why it's safe on a live production table — no maintenance window needed.

9 · OPTIMIZE returns file statistics

OPTIMIZE returns a row of file statistics — min/max/total size of files removed and added, Z-ordering stats (when used), the number of batches, and the partitions optimized. The same metrics land in DESCRIBE HISTORY under operationMetrics. Pair them with DESCRIBE DETAIL (numFiles, sizeInBytes) for a clean before/after — measure the win instead of guessing.

SQL-- OPTIMIZE returns stats (files removed/added, batches, partitions optimized).
OPTIMIZE main.delta_opt_demo.events;

-- The same numbers land in DESCRIBE HISTORY under operationMetrics:
DESCRIBE HISTORY main.delta_opt_demo.events;   -- latest OPTIMIZE row → operationMetrics
DESCRIBE DETAIL  main.delta_opt_demo.events;   -- numFiles / sizeInBytes before/after

10 · Predictive optimization runs it automatically; frequency & compute

On UC managed tables, predictive optimization runs OPTIMIZE automatically when it's cost-effective — no scheduled job (Lesson 09). When you run it manually, frequency is a cost/performance trade-off: Databricks recommends start daily, then tune. OPTIMIZE is CPU-intensive (Parquet decode/encode) — use compute-optimized instances and attached SSDs. The target size the rewrite aims for is set by delta.targetFileSize / autotuning (Lesson 07).

SQL-- Let the platform run OPTIMIZE for you on UC MANAGED tables (Lesson 09):
ALTER SCHEMA main.delta_opt_demo ENABLE PREDICTIVE OPTIMIZATION;

-- Optional: pin the target file size the rewrite aims for (else autotuned — Lesson 07).
ALTER TABLE main.delta_opt_demo.events
  SET TBLPROPERTIES ('delta.targetFileSize' = '256mb');

Comparison — OPTIMIZE variants & related maintenance

OperationWhat it doesIdempotent?ScopeUse when
OPTIMIZE tBin-pack small files into fewer right-sized filesYesWhole tableRoutine compaction of a fragmented table
OPTIMIZE t WHERE …Compact only matching partitionsYesPartition subsetBig partitioned table; only recent partitions fragmented
OPTIMIZE t ZORDER BY (…)Compact + colocate via Z-orderNoTable / subsetNon-LC table filtered on high-cardinality cols
OPTIMIZE t (LC table)Compact + group by clustering keys (incremental)IncrementalWhole tableRoutine upkeep of a liquid-clustering table
OPTIMIZE t FULLFull recluster to current keys (DBR 16.0+)n/a (forces rewrite)Whole tableRight after enabling LC or changing keys
Predictive optimizationAuto-runs OPTIMIZE when cost-effectiven/aUC managed tablesThe default for new managed tables (Lesson 09)

Uses, edge cases & limitations

Uses

Edge cases an interviewer probes

Limitations

Common gotchas