Microsoft Fabric · Delta Lake · Data Engineering

Delta Lake Optimization in Microsoft Fabric: OPTIMIZE, VACUUM, V-Order and Liquid Clustering

A practical production guide to small-file control, table layout, deletion vectors, safe retention, and automated maintenance across Spark, the SQL analytics endpoint, and Power BI Direct Lake.

Practitioner walkthrough Spark SQL and PySpark Runtime 2.0 guidance Reviewed 31 Oct 2026

A Fabric Lakehouse can start fast and then slow down as the data grows, even when the business query hasn’t changed. A common cause is the physical shape of the Delta table: too many small Parquet files, a layout that doesn’t suit the filters people use, repeated updates, and obsolete files that remain in OneLake.

Optimizing a Delta table isn’t one command. OPTIMIZE compacts and reorganises active files. Auto compaction stops small files building up in the first place. V-Order can help read-heavy Fabric consumers. Liquid clustering helps file skipping. Deletion vectors make row-level changes cheap. VACUUM removes files that are no longer referenced once the retention window has passed. Each one solves a different problem, and this guide shows you which to reach for.

What this guide helps you do: pick the right layout for the engines that read the table, spot small-file fragmentation, run maintenance safely, automate it, and check that the change actually helped instead of just burning compute.
Scope: this is about Delta tables in a Fabric Lakehouse. Don’t run Spark OPTIMIZE, REORG, or VACUUM against a Fabric Warehouse table or through the SQL analytics endpoint. Warehouse manages its own layout and compaction, and database mirroring manages its destination layout too. Section 12 covers both.

1. Why Delta table maintenance matters

Fabric Lakehouse tables use Delta Lake transaction metadata over Parquet data files. Each committed version describes which files belong to the current snapshot. That design gives you reliable reads while writes, updates, merges, and deletes create new versions.

Over time, frequent small writes create many small Parquet files. A query then spends more effort listing files, reading footers, and planning tasks. Repeated MERGE, UPDATE, and DELETE operations also leave older files unreferenced, or mark rows deleted through deletion-vector metadata. Microsoft is blunt about this: even append-only tables that perform well at first can pick up excessive small files, and that affects Spark, the SQL analytics endpoint, Direct Lake, and external readers.

Illustration: why compaction helps
S1
S2
S3
S4
S5
S6
S7
S8

Before: frequent small writes leave many files for readers and the planner to handle.

OPTIMIZE · BIN COMPACTION ↓
LARGE A
LARGE B
LARGE C

After: the same logical rows sit in fewer, better-sized files. This is conceptual. Real output depends on table size, workload, and runtime settings.

1. File compaction

Combine many small files into fewer larger ones to cut metadata and task overhead. Commands: OPTIMIZE and auto compaction.

2. Data organisation

Arrange data so selective queries can skip files. Liquid clustering for most new tables, or Z-Order for suitable existing partitioned ones.

3. Storage cleanup

Delete old files that the Delta log no longer references. Command: VACUUM. It saves storage, not query time.

These are complements, not substitutes. OPTIMIZE doesn’t remove every obsolete file from storage. VACUUM doesn’t compact the active layout. Clustering only helps when its columns match real filters.

2. Choose the strategy by producer and consumer

Advertisements

A Delta table in OneLake can be written by Spark, Copy activity, or Dataflow Gen2, and read by Spark, the SQL analytics endpoint, or Direct Lake. The writer and the readers decide who owns the physical layout and which optimisations make sense.

ProducerMain consumerMicrosoft’s recommended starting point
Spark notebook or jobSparkUse Runtime 2.0 or later defaults and enable auto compaction. Consider liquid clustering when measured predicates benefit from file skipping.
Spark notebook or jobSQL analytics endpointUse the same layout as for Spark. Don’t set a static file size, an arbitrary row limit, or V-Order solely for SQL endpoint performance.
Spark notebook or jobPower BI Direct LakeSame layout as Spark, plus V-Order, or use the readHeavyForPBI resource profile.
Copy activity or Dataflow Gen2Spark, SQL endpoint, or Direct LakeInspect the resulting file layout and schedule compatible Lakehouse maintenance separately. Some modes, such as Dataflow Gen2 incremental refresh, restrict maintenance.
Fabric WarehouseWarehouse or SparkUse the system-managed layout. Warehouse compacts automatically. Use data clustering for recurring selective predicates.
Database mirroringAny Fabric engineFabric manages the V-Ordered layout. There are no direct layout controls on the destination.

Microsoft’s wider advice is to reuse one shared table wherever consumers’ needs are compatible, rather than copying a table for every engine. It suggests a separate table only for an independent purpose, such as a different grain, different security or retention, a latency need the shared table can’t meet, or a consumer-specific layout whose measured benefit outweighs the storage, processing, and governance cost.

Dataflow Gen2 caveat: a Dataflow Gen2 Lakehouse destination that uses incremental refresh doesn’t support OPTIMIZE or REORG TABLE. Check the destination mode before you schedule either one.

3. Diagnose table health before running maintenance

Don’t schedule a full-table OPTIMIZE just because a table exists. Look at its size, file count, recent operations, and workload first. Run these in a Fabric notebook with a Spark SQL cell, replacing the example table name.

Inspect table detail and history

DESCRIBE DETAIL sales.orders;

DESCRIBE HISTORY sales.orders LIMIT 20;

DESCRIBE DETAIL shows file count and total size, plus the evaluated adaptive file size property. DESCRIBE HISTORY shows whether the table is mostly appends, merges, deletes, overwrites, or earlier maintenance runs. A high file count isn’t automatically a problem, because it depends on table size and query pattern. A file count that climbs faster than the table’s size is the signal.

Check average file size across several tables

Average file size is a quick first indicator. This notebook cell loops over a list of tables and prints a small report. Replace the list with your own tables. The threshold is your own choice for flagging tables to look at, not Microsoft guidance.

tables = ["sales.orders", "sales.customers", "sales.order_lines"]
flag_below_mb = 64

rows = []
for name in tables:
    detail = spark.sql("DESCRIBE DETAIL " + name).first()
    size_bytes = detail["sizeInBytes"]
    num_files = detail["numFiles"]
    avg_mb = (size_bytes / num_files / 1048576) if num_files else 0
    rows.append((name, num_files, round(size_bytes / 1073741824, 2), round(avg_mb, 1), flag_below_mb > avg_mb))

report = spark.createDataFrame(rows, ["table_name", "num_files", "size_gb", "avg_file_mb", "look_closer"])
display(report)

An average can hide skew between partitions, or between recent files and ones you compacted earlier. If a table is flagged, look at the file distribution, or use Delta Analyzer for Direct Lake tables, before you change any settings.

Record a baseline

  • Table size and active file count from DESCRIBE DETAIL.
  • Recent transactions from DESCRIBE HISTORY.
  • Typical read query duration and scan behaviour for the real consumers.
  • Ingestion frequency, micro-batch size, and peak write periods.
  • Whether a Direct Lake model reads this table and when it is framed or refreshed.

Match the signal to the action

SignalLakehouse action
File count rises faster than table size, files stay below the adaptive targetRun a one-time OPTIMIZE for the backlog, then enable auto compaction. For Copy or Dataflow Gen2 writes, schedule maintenance separately.
Files far larger than the current adaptive target, and scan parallelism is limitedRewrite the table with an overwrite or CREATE OR REPLACE TABLE AS SELECT, with adaptive target file size enabled.
Deletion vectors accumulateKeep auto compaction on. If it isn’t triggered, schedule OPTIMIZE. Use REORG TABLE ... APPLY (PURGE) only for explicit purge needs.
Selective queries scan a big share of the tableConfigure liquid clustering, or Z-Order for an existing partitioned table.
Direct Lake shows excess files, small row groups, or broad re-transcodingCompact small files, review row groups, and apply V-Order to Spark-written tables.
OneLake storage grows faster than the active tableRun VACUUM on its own schedule, within your retention policy.
Practitioner tip: keep a before and after record for one representative query. If the file count falls but query time doesn’t, the bottleneck is probably joins, skew, poor predicates, model design, or capacity. It isn’t the file layout.

4. Run OPTIMIZE to compact Delta files

OPTIMIZE is the core compaction command. It groups small files into bins that target an ideal size and rewrites them. It changes the physical layout and never the logical rows.

Basic compaction

OPTIMIZE sales.orders;

Use it for a one-time cleanup of a backlog, or in a planned maintenance window. By default minFileSize and maxFileSize are both 1 GB. OPTIMIZE is idempotent, but Microsoft warns that an oversized minFileSize can increase write amplification. With the minimum at 1 GB, a 900 MB file may get rewritten after one small extra write. For automatic size management, rely on adaptive target file size instead of hand-tuning.

Optimize a scoped part of a partitioned table

-- Only for a table that is already partitioned by order_date.
OPTIMIZE sales.orders
WHERE order_date >= DATE '2026-10-01';

Scoped maintenance helps when a partitioned table only needs a known range compacted. Don’t add partitioning just to make this possible. Over-partitioning creates more small files.

Z-Order and V-Order options

OPTIMIZE sales.orders ZORDER BY (customer_id, order_date) VORDER;

When combined, Spark applies bin compaction first, then Z-Order, then V-Order. Pick the Z-Order columns from measured, recurring predicates. Z-Order doesn’t apply to liquid clustered tables (see section 6).

How you call itV-Order behaviour
OPTIMIZE table VORDERForces V-Order on the rewritten files, whatever the session or table settings say.
OPTIMIZE table with no keywordInherits from the table property delta.parquet.vorder.enabled if set, otherwise from the session setting spark.sql.parquet.vorder.default.
Don’t tune by superstition: Z-Order rewrites data and costs compute. If people don’t filter selectively on those columns, you pay for little benefit. For new or changing tables, look at liquid clustering first.

Fast Optimize

Fast Optimize looks at each candidate bin of small files and skips compaction when it is unlikely to help. It only compacts a bin if merging is likely to reach the minimum target size or the bin holds too many small files. That lowers write amplification and makes repeated runs more idempotent.

SET spark.microsoft.delta.optimize.fast.enabled = TRUE;
SettingDefaultWhat it does
spark.microsoft.delta.optimize.fast.minNumFiles50Small files needed in a bin to compact it even if it is unlikely to reach the target size.
spark.microsoft.delta.optimize.fast.parquetCoefficient1.3Multiplied by the minimum file size to set the minimum small-file data a bin needs. Raise it to skip bins less often, lower it to skip more.

Microsoft’s compaction page says Fast Optimize doesn’t apply to liquid clustering and Z-Order operations, and doesn’t change auto compaction. Its liquid clustering page says it is compatible with liquid clustered tables from Runtime 2.0. Those two statements read differently, so test on your runtime before relying on either.

File-level compaction targets

This setting stops files that already met an earlier target from being rewritten again when the target changes, for example when adaptive file size picks a larger one. Microsoft recommends enabling it to limit write amplification, though it is off by default.

SET spark.microsoft.delta.optimize.fileLevelTarget.enabled = TRUE;

When to use OPTIMIZE FULL

On a liquid clustered table, standard OPTIMIZE doesn’t recluster files that already count as clustered. OPTIMIZE FULL forces a table-wide reclustering pass, and it needs Runtime 2.0 or later. Use it after you change clustering columns, or when you migrate a table between clustering providers. It can rewrite most or all of a large table, so run it deliberately, not as routine compaction.

OPTIMIZE sales.orders FULL;

5. Prevent the small-file problem before it grows

For Spark-written tables, Microsoft’s default advice is to enable auto compaction and let it do the routine work. It checks partition health after each write and, only when it finds too many small files, runs a synchronous OPTIMIZE straight after the write commits. That means you don’t need a separate table-health check before maintenance runs. Scheduled OPTIMIZE is for the exceptions.

ScenarioRecommended approach
Spark-written tableEnable auto compaction as the default maintenance strategy.
Streaming or micro-batch writesAuto compaction plus optimize write, to reduce small-file build-up.
Strict write-latency requirementsSchedule OPTIMIZE separately instead of synchronous auto compaction.
Existing table with a pile of small filesOne-time OPTIMIZE, then enable auto compaction.
Frequent updates, deletes, or mergesKeep deletion vectors and auto compaction on.

Enable auto compaction

The session setting only applies to new tables created in that Spark session, so for tables that already exist, use the table property.

-- Session level: affects new tables created in this session
SET spark.databricks.delta.autoCompact.enabled = TRUE;

-- Table level, on an existing table
ALTER TABLE sales.orders
SET TBLPROPERTIES ('delta.autoOptimize.autoCompact' = 'true');

You can also set it when creating a table, with the DataFrame writer option delta.autoOptimize.autoCompact or the same table property in CREATE TABLE.

Reduce evaluation overhead on busy tables

By default auto compaction checks file metadata after every write. From Runtime 2.0 you can defer that check to log checkpoints, typically every 10 commits, which lowers per-commit overhead while still compacting periodically.

SET spark.microsoft.delta.autoCompact.onCheckpointOnly.enabled = TRUE;

Use adaptive file sizing, not arbitrary limits

Leave adaptive target file size on. It picks a target for each table between 128 MB and 1 GB. Don’t impose a maximum row count per file, because row width varies and a row cap creates tiny files for narrow tables. In Runtime 1.3, adaptive target file size, file-level targets, and deletion vectors are opt-in, while Runtime 2.0 and later turn them on by default.

Liquid clustering and auto compaction in Runtime 1.3: don’t combine them. Every auto compaction trigger can rewrite whole clustering units, which causes severe write amplification. In Runtime 2.0 and later they work together.

6. Liquid clustering, Z-Order, and partitioning

Advertisements

Compaction reduces the number of files. Data organisation decides whether a query can skip files that can’t contain its values. The right choice depends on how filters and joins really look.

Liquid clustering: the flexible default for new tables

Liquid clustering stores the clustering definition in table metadata. Unlike partitioning it creates no directory per value, handles high-cardinality columns well, and lets you change columns later. One thing catches people out: regular writes don’t cluster the data. Clustering is applied only when OPTIMIZE runs, so you need auto compaction or a schedule for the benefit to appear.

-- New table
CREATE TABLE dbo.sales (
    order_id BIGINT,
    order_date DATE,
    region STRING,
    amount DECIMAL(10,2)
)
CLUSTER BY (order_date, region);

-- Existing table, then apply the layout
ALTER TABLE sales.orders CLUSTER BY (order_date, region);
OPTIMIZE sales.orders;

-- Change or remove the clustering later
ALTER TABLE sales.orders CLUSTER BY (region, product_category);
ALTER TABLE sales.orders CLUSTER BY NONE;

After you change the columns, existing files keep their old layout until reclustered, so use OPTIMIZE FULL once if you want the whole table rebuilt.

Runtime 2.0 changes the economics

  • Runtime 2.0 and later: incremental clustering is on by default. OPTIMIZE only processes unclustered, small, unhealthy, or deletion-vector files, and auto reclustering handles overlapping value ranges. Frequent runs are cheap.
  • Runtime 1.3: every OPTIMIZE rewrites all files in each clustering unit under 100 GB, even when already well clustered. For tables under 100 GB that rewrites everything every time. Keep runs deliberate and infrequent.
  • On Runtime 2.0, the native execution engine can run OPTIMIZE on liquid clustered tables, which Microsoft says is 30 to 50 percent faster for multi-dimensional clustering.

Choosing clustering columns

  • Pick one to four columns that show up often in WHERE clauses. More columns dilute file skipping and increase clustering time.
  • Column order after CLUSTER BY makes no difference.
  • Eligible types include numeric, date, timestamp, and string types. Binary, boolean, and null types aren’t eligible, and complex types need a setting from Runtime 2.0.
  • With two or more columns Fabric uses a Hilbert curve, and with a single column it uses Z-Order.

Check clustering quality (Runtime 2.0 and later)

The clusteringQuality() method returns one row per clustering column. Compare it before and after OPTIMIZE. It estimates file-skipping potential from statistics and doesn’t measure a particular query’s speed. It’s a Scala call.

import io.delta.tables.DeltaTable

val deltaTable = DeltaTable.forName(spark, "dbo.sales")
display(deltaTable.clusteringQuality())

Look for avg_depth near 1.0, an overlap_ratio near 0, and num_files_with_stats close to num_files.

Z-Order and partitioning: not compatible with liquid clustering

Microsoft’s documentation is explicit that liquid clustering is incompatible with both partitioning and Z-Order. For file skipping on a clustered table, liquid clustering replaces them. That makes the choice a design decision, not an add-on.

StrategyBest fitWatch out for
Liquid clusteringNew or evolving tables where recurring filters benefit from skipping and the columns may change.Needs OPTIMIZE or auto compaction to apply the layout. Incremental behaviour needs Runtime 2.0 or later.
Z-OrderExisting partitioned tables with selective filters on a few recurring columns within a partition.No persistent definition and no incremental mode. Costly to repeat. Incompatible with liquid clustering.
Hive-style partitioningA specific need such as isolating concurrent writers that touch disjoint partitions.Over-partitioning creates small files. Avoid by default. Incompatible with liquid clustering.

7. V-Order and Power BI Direct Lake

V-Order is a Parquet layout optimisation for Fabric’s analytical engines. It matters most for Power BI Direct Lake, where file and row-group layout affects column loading and transcoding. It isn’t free, because it adds write-time cost, and Microsoft’s guidance is not to enable it solely for Spark or SQL analytics endpoint performance. For Spark-written tables mainly read through Direct Lake, enable V-Order or use the readHeavyForPBI resource profile.

# Use in a Spark session dedicated to a Power BI serving workload
spark.conf.set("spark.fabric.resourceProfile", "readHeavyForPBI")

New workspaces default to the writeHeavy profile. Use readHeavyForSpark for frequent Spark reads, and keep write-heavy for ingestion unless measurements say otherwise.

Row groups and update patterns matter for Direct Lake

  • Direct Lake generally performs best with row groups between 1 million and 16 million rows. Small or uneven row groups create more column segments and more transcoding work.
  • For Spark-written tables, spark.sql.parquet.native.writer.maxRowGroupRowCount sets the maximum rows per row group when the native execution engine writes Parquet. The default is 0, meaning no maximum. Only set a tested limit after analysis shows row-group size is hurting performance.
  • Prefer append-friendly update patterns. They preserve existing Parquet files and support incremental framing, so less is re-transcoded.
  • Use Delta Analyzer to inspect row groups and update patterns.
spark.conf.set("spark.sql.parquet.native.writer.maxRowGroupRowCount", 8000000)
Direct Lake and VACUUM: a framed Direct Lake model can reference a particular Delta version. Keep the files that version needs until the model is reframed to a newer one. If VACUUM removes files a framed model still needs, users can hit query errors. Coordinate framing and retention before you shorten retention. Also expect that a large OPTIMIZE rewrite means the next query has more to re-transcode, so schedule big rewrites when that short cold period is acceptable.

8. Deletion vectors and REORG TABLE APPLY (PURGE)

Deletion vectors let Delta mark rows deleted without rewriting every affected Parquet file straight away. Updates and deletes get faster, but the bytes can stay in the original files until a later rewrite. Readers honour the metadata, so logically deleted rows are never returned in the meantime.

For routine work, OPTIMIZE is usually enough. It purges a file’s deletion vectors during compaction when more than 5 percent of that file’s records are referenced by them. Auto compaction purges them only when the partition also meets its small-file trigger. So if a workload runs updates or deletes without creating small files, schedule OPTIMIZE now and then.

REORG TABLE sales.orders APPLY (PURGE);

Use REORG when you must physically remove soft-deleted rows below the automatic threshold, or meet a specific compliance need. It rewrites affected active files. The older replaced files stay in storage until they become eligible for VACUUM.

Logical deletion isn’t physical erasure. A DELETE changes the table’s logical result. REORG ... APPLY (PURGE) rewrites active files without the deleted rows. VACUUM later removes the replaced files after the retention period. For compliance, check the full lifecycle, including backups, downstream exports, and retention duties, not just the delete statement.

9. Run VACUUM safely

VACUUM permanently deletes data files that the Delta log no longer references and that are older than the retention threshold. Microsoft is clear that it saves storage and doesn’t make queries faster by itself. It runs in Spark experiences (notebooks, Spark job definitions, the Lakehouse Maintenance UI, and pipeline maintenance), and not in the SQL analytics endpoint or the Warehouse SQL editor.

Preview first, then run

VACUUM sales.orders DRY RUN;

VACUUM sales.orders RETAIN 168 HOURS DRY RUN;

VACUUM sales.orders;

The default retention is seven days (168 hours). Run the dry run, then confirm your time-travel, concurrent-reader, concurrent-writer, and Direct Lake framing needs before the real command.

Faster options for large tables (Runtime 2.0 and later)

-- Uses only the Delta log to find files, instead of listing the directory
VACUUM sales.orders LITE;

-- For very large tables: supply a precomputed file inventory
VACUUM sales.orders USING INVENTORY inventory_table_name;

LITE is much faster on large tables but needs enough log history. If the log has been pruned too far it raises DELTA_CANNOT_VACUUM_LITE, and you should fall back to a standard VACUUM. The inventory table needs the columns path, length, isDir, and modificationTime.

Why the seven-day guard exists

A shorter retention can remove files that a long-running reader, a streaming job, or a recovery still needs, and it ends time travel beyond the window. Spark has a retention safety check controlled by spark.databricks.delta.retentionDurationCheck.enabled, and Fabric’s maintenance UI and REST API reject retention under seven days by default. Don’t switch the check off just to make a job pass.

What VACUUM doesn’t do

  • It doesn’t remove files the current table state still references, including ones still referenced through deletion-vector metadata.
  • It doesn’t delete Delta log files in _delta_log. Log cleanup follows separate checkpoint and log retention.
  • It doesn’t improve the active layout, so run it after OPTIMIZE to clear the replaced pre-compaction files.
Never use short retention as a storage-saving trick. Check your time-travel promise, longest-running query, streaming readers, model framing, and recovery window first.

10. Automate maintenance in a Fabric pipeline

For recurring production workloads, put maintenance in the pipeline, not in someone’s memory. Fabric’s Lakehouse Maintenance activity runs Optimize and Vacuum as an orchestrated step. Microsoft’s September 2026 Fabric feature summary lists it as generally available.

A practical Lakehouse pipeline sequence
1 · Copy or ingestLoad source data
→
2 · NotebookMERGE, clean, write Delta
→
3 · MaintenanceOptimize, and Vacuum on its own cadence
→
4 · Refresh endpointWhen the workflow needs it
→
5 · ServePower BI, SQL, downstream

Configure the activity

  1. Create or open a pipeline in the workspace that holds the Lakehouse.
  2. Search the Activities pane for Lakehouse Maintenance and add it.
  3. In Settings, choose or create a connection, then select the Lakehouse.
  4. Set the table name, and the optional schema name where available.
  5. Choose Optimize or Vacuum. Keep any Vacuum retention aligned with your approved policy.
  6. Save, run in a non-production workspace first, then schedule it to match how the table is loaded.
  7. Review the Output tab and the Monitoring hub, and use success and failure conditions to stop downstream steps if maintenance fails.

The activity runs synchronously, so it only completes when the maintenance operation does, and it returns a failure if maintenance fails for a table. The documentation describes choosing Optimize or Vacuum. If you need finer control, such as Z-Order columns or a liquid clustering step, run the commands from a notebook activity instead.

Known issues in Microsoft’s reference: the activity doesn’t support Lakehouses with schemas enabled yet, and it doesn’t currently support workspaces with Private Link enabled. The same page also describes an optional schema-name field, so treat the schema detail as unclear and test in your tenant before you design around it. The table-maintenance REST API takes a schema name, so don’t assume the activity and the API behave identically.

Don’t optimise every table on every run

Base the policy on table behaviour. A busy event table may need frequent compaction, while a slowly changing dimension may need almost none. Schedule VACUUM separately if its retention differs from the compaction cadence, and avoid a full rewrite right after every small append.

11. Automate table maintenance with the Fabric REST API

If maintenance must be called from a deployment tool or a custom scheduler, Microsoft exposes a Table Maintenance API on the Lakehouse jobs endpoint. It’s asynchronous: submit the request, read the polling URI from the Location response header, and poll until the job finishes.

Submit a maintenance request

POST https://api.fabric.microsoft.com/v1/workspaces/WORKSPACE_ID/lakehouses/LAKEHOUSE_ID/jobs/TableMaintenance/instances
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json

{
  "executionData": {
    "tableName": "orders",
    "schemaName": "sales",
    "optimizeSettings": {
      "vOrder": true,
      "zOrderBy": ["customer_id"]
    },
    "vacuumSettings": {
      "retentionPeriod": "7:00:00:00"
    }
  }
}

Replace the IDs, table, and columns with real values. The retention period uses a days:hours:minutes:seconds format, so 7:00:00:00 is seven days. Include schemaName only for a schema-enabled Lakehouse. Remember that Z-Order settings don’t apply to liquid clustered tables.

Poll the job

GET https://api.fabric.microsoft.com/v1/workspaces/WORKSPACE_ID/items/LAKEHOUSE_ID/jobs/instances/OPERATION_ID
Authorization: Bearer ACCESS_TOKEN

The run endpoint is Lakehouse-scoped, but polling uses the generic item job route, items and not lakehouses. That’s by design. Handle the statuses NotStarted, InProgress, Completed, Failed, Canceled, and Deduped. Deduped means a job of the same type was already running and yours was skipped, so treat it as its own state and don’t assume the work ran twice.

Implementation note: get a Microsoft Entra token for the Fabric API and give the calling identity the permissions it needs. Never put client secrets in notebooks, HTML examples, or public repositories. Follow Microsoft’s current API reference for scopes, throttling, and errors.

12. Warehouse and mirrored data: what not to do

Not everything in Fabric is managed with these commands. Check who owns the layout before you touch it.

  • Fabric Warehouse: it manages compaction and the physical layout automatically. Use data clustering for recurring selective predicates, and keep V-Order on for read-oriented and mixed workloads (it’s on by default). Disabling V-Order is a warehouse-level, irreversible change, so test the whole read and write workload first.
  • Database mirroring: Fabric replicates the source into Delta tables with a system-managed V-Ordered layout. You can’t directly configure file size, deletion-vector cleanup, clustering, partitioning, or V-Order. Design queries and Direct Lake models to suit the layout you get.
  • Mirrored catalogs: Fabric uses shortcuts to reference the source files in place and doesn’t rewrite or maintain them. Improve the layout in the source system where its features allow. For Direct Lake, consider an extra V-Ordered serving layer when the source layout can’t meet your performance needs.

13. Validate the result instead of assuming success

Maintenance only succeeded if it met its goal without breaking the consumers. A green pipeline status is necessary, not sufficient.

Compare table metadata

-- Capture before and after
DESCRIBE DETAIL sales.orders;
DESCRIBE HISTORY sales.orders LIMIT 20;

History and operation output show metrics such as files added and removed and bytes added and removed. Use them to answer practical questions:

  • Did compaction actually reduce the file count?
  • Did it rewrite far more data than recent ingestion justified?
  • Did the median file size move the right way?
  • Did it keep rewriting files that were already healthy?
  • Did the next write recreate the small-file problem immediately?

Re-run representative queries

Use the same filters, columns, query shape, and comparable cache conditions as the baseline, and record cold and warm behaviour separately. For Direct Lake, test the real semantic model and framing path, not only a Spark query on the table.

SignalWhat to look forWhat it may mean
Files added and removedMany small files consolidated into fewer.Compaction changed the layout. Verify reads.
Bytes rewrittenA large rewrite relative to recent ingestion.Review target size, clustering, and whether maintenance is repeating needlessly.
Query scan and latencySame filters scan fewer files or run faster.A likely layout benefit. Compare like with like.
Direct Lake framing and queryRefresh succeeds and queries hit the expected version.Check V-Order, row groups, framing, and retention.
Storage after VACUUMEligible unreferenced files are gone after the window.Cleanup worked. It doesn’t show query acceleration.

14. Production runbook and starting policy

Before maintenance

  • Confirm the target is a Lakehouse Delta table and not a Warehouse-managed or mirrored one.
  • Identify the writer and the important consumers: Spark, the SQL endpoint, Direct Lake, or external readers.
  • Capture file count, size, history, a representative query time, and ingestion frequency.
  • Check table properties, partitioning, deletion vectors, clustering keys, and the runtime version.
  • Agree the retention window with recovery, time travel, audit, long-running readers, and Direct Lake framing.
  • Try layout or purge changes on a development copy of critical tables first.

During maintenance

  • Use auto compaction for routine upkeep and OPTIMIZE for backlogs, exceptions, and clustering.
  • Choose V-Order for Direct Lake or other read-heavy consumers, not as a universal default.
  • Pick liquid clustering or Z-Order, not both, from measured predicates.
  • Run VACUUM ... DRY RUN before deleting, and keep the seven-day guard unless a tested requirement says otherwise.
  • Don’t overlap runs for the same table without understanding the concurrency and capacity impact.

After maintenance

  • Inspect the job status and Delta history.
  • Check row counts, key totals, and business aggregates.
  • Refresh the SQL analytics endpoint where downstream steps need it.
  • Reframe or refresh Direct Lake models and test the real reports.
  • Record files added and removed, bytes rewritten, duration, and workload timing, then tune the schedule from what you saw, not an arbitrary timer.

A starting policy for a new Lakehouse

There is no universal schedule. Treat this as a starting policy to test, not a Microsoft-mandated configuration.

Workload patternStarting approachReview trigger
Frequent micro-batch appendAuto compaction, plus optimize write on the write path.Write latency rises or file counts keep climbing.
Large daily batchAuto compaction. Add a scheduled OPTIMIZE only if the batch leaves fragmentation behind.File count and read time after big loads.
Direct Lake serving tablesTest V-Order or readHeavyForPBI, check row groups, coordinate framing.Cold-cache query, transcoding, and refresh behaviour.
Frequent MERGE and DELETEKeep deletion vectors and auto compaction. Schedule OPTIMIZE if deletion vectors accumulate.Deletion-vector build-up, rewrite cost, compliance needs.
Storage cleanupSchedule VACUUM separately, at least the default seven days of retention.Storage growth, time-travel needs, longest readers, recovery window.

Medallion layers

Bronze, Silver, and Gold describe purpose, and they don’t decide whether the layout is user-managed or system-managed. As a guide, Bronze favours write throughput, so keep Spark-written tables on auto compaction and avoid building Direct Lake models directly on raw Bronze unless you designed for it. Silver is reused across compatible consumers, and V-Order there only makes sense when Direct Lake is a main consumer. Gold is the preferred layer for Direct Lake models.

A sensible pilot: pick three tables, one busy append table, one merge-heavy table, and one Direct Lake serving table. Run the health-check cell from section 3, apply the matching starting approach, and compare file count, a representative query, and capacity use after a week. Standardise only after that.

15. Troubleshooting

OPTIMIZE succeeds but queries are still slow

Compaction only fixes file layout. Check whether the query filters selectively, whether clustering matches the predicates, whether joins are skewed, and whether the Spark plan or semantic model is the real bottleneck. Compare under similar cache conditions.

Small files come back right after OPTIMIZE

The write path may be creating new small files on every run. Look at ingestion frequency, micro-batch size, partition count, and writer settings. Use auto compaction and optimize write for Spark writers, or schedule maintenance after Copy and Dataflow Gen2 writes.

VACUUM fails or warns about retention

Check the requested interval and the safety check. The UI and API reject under seven days by default. Don’t turn the guard off without validating readers, writers, framing, and recovery needs.

VACUUM LITE raises DELTA_CANNOT_VACUUM_LITE

The Delta log has been pruned beyond what LITE needs. Fall back to a standard VACUUM, and confirm your runtime supports LITE at all.

Direct Lake queries fail after cleanup

The framed model may point at a version whose files were removed. Reframe it to the current version and make sure retention protects versions that active consumers still need.

Liquid clustered table isn’t skipping files

Regular writes don’t cluster data. Check that OPTIMIZE or auto compaction is running, run clusteringQuality() on Runtime 2.0, and look at whether the clustering columns match real filters. If you changed columns, run OPTIMIZE FULL once.

The Lakehouse Maintenance activity fails

Confirm permissions and the connection. Check whether the Lakehouse has schemas enabled or the workspace has Private Link, since both are listed as limitations, and read the Output tab for the failing table.

REORG PURGE didn’t free storage

REORG ... APPLY (PURGE) rewrites active files. The replaced files stay until they are old enough for VACUUM, so physical removal needs the right sequence and the retention window to pass.

Frequently asked questions

What is the difference between OPTIMIZE and VACUUM in Microsoft Fabric?

OPTIMIZE rewrites active Delta data files to compact small files and can apply layout options such as V-Order, Z-Order, or liquid clustering. VACUUM permanently removes unreferenced data files older than the retention threshold. OPTIMIZE changes the active layout, and VACUUM is storage cleanup.

Should I schedule OPTIMIZE or turn on auto compaction?

For Spark-written tables, Microsoft recommends enabling auto compaction as the default strategy, because it only compacts when a partition is fragmented. Schedule OPTIMIZE separately for strict write-latency workloads, for a one-time backlog, or to purge deletion vectors when no small-file trigger fires.

Can I run OPTIMIZE or VACUUM from the SQL analytics endpoint?

No. These are Spark Delta maintenance commands. Run them from a Fabric notebook, a Spark job definition, the Lakehouse maintenance UI, a pipeline maintenance activity, or the table-maintenance REST API. Don’t run them in the SQL analytics endpoint or the Warehouse SQL editor.

What is the default VACUUM retention period in Fabric?

Seven days, or 168 hours. A shorter period can remove files needed for time travel, concurrent readers, recovery, or a framed Direct Lake model. Use DRY RUN and review dependencies before cleanup.

Does OPTIMIZE improve query performance every time?

No. It helps when file fragmentation is a real bottleneck, but it costs compute and rewrites files. It may not help queries limited by joins, skew, model design, capacity, or nonselective predicates. Measure before and after.

Should I always enable V-Order?

No. V-Order matters most for Direct Lake and read-oriented Fabric consumption. Microsoft does not recommend enabling it solely for Spark or SQL analytics endpoint performance. Weigh the consumer against the write-time cost.

Can I use Z-Order and liquid clustering together?

No. Microsoft’s documentation lists Z-Order and partitioning as incompatible with liquid clustering. Use liquid clustering for flexible file skipping on new or evolving tables, and Z-Order only on suitable existing partitioned tables.

Why isn’t my liquid clustered table clustered after a write?

Regular write operations don’t cluster the data. Clustering is applied when OPTIMIZE runs, so you need auto compaction or a scheduled OPTIMIZE. On Runtime 2.0 and later, incremental clustering keeps those runs inexpensive.

What does REORG TABLE APPLY (PURGE) do?

It rewrites files that contain rows marked deleted by deletion vectors, so those rows are physically removed from the rewritten Parquet files. Use it for a specific compliance or purge requirement. VACUUM later removes the replaced files after retention.

Can I automate Delta table maintenance in a Fabric pipeline?

Yes. Fabric Data Factory has a Lakehouse Maintenance activity for Optimize and Vacuum. Place it after ingestion or transformation and monitor it through pipeline run history. Check the documented limits for schema-enabled Lakehouses and Private Link workspaces first.

Does VACUUM delete the Delta transaction log?

No. VACUUM removes eligible unreferenced data files. Delta log files in _delta_log follow separate checkpoint and log retention behaviour.

Official Microsoft documentation and further reading

Use Microsoft’s documentation for current syntax, runtime behaviour, and limits. Capabilities change, so check details on the runtime and tenant where you plan to run maintenance.

Related Ultimate Info Guide tutorials

Final takeaway

Good Delta Lake maintenance is a workload decision, not a ritual. Diagnose the table, choose the layout from who reads it, let auto compaction handle routine small files, use OPTIMIZE for backlogs and clustering, and run VACUUM only after you have reviewed the recovery and time-travel promise. Automate the repeatable steps, then measure what the consumers actually experience.

Scroll to Top