Databricks Liquid Clustering vs Z-ORDER and Partitioning

Databricks Liquid Clustering vs Z-ORDER and Partitioning

For years, Delta Lake table layout meant two choices made on day one: which column to partition by, and which columns to Z-ORDER. Get either wrong and you were stuck with thousands of tiny partitions, or a nightly OPTIMIZE ZORDER BY job that rewrote the whole table and still left queries scanning too many files. Liquid clustering replaces both with one flexible layout that you can change later without rewriting history.

This guide is for data engineers running Delta tables on Databricks. It covers what liquid clustering does, how it compares with partitioning and Z-ORDER, the SQL you need to create, migrate and maintain clustered tables, and the pitfalls that catch teams out in production.

Why partitioning and Z-ORDER struggle

Hive-style partitioning works well when you have one low-cardinality column that almost every query filters on, such as event_date. It breaks down when:

  • The partition column has high cardinality (customer ID, device ID), creating many small files and slow metadata listing.
  • Data is skewed, so a few partitions hold most of the rows.
  • Query patterns change and a different column becomes the main filter. Repartitioning means rewriting the whole table.

Z-ORDER helps by co-locating related values inside files so data skipping can prune more. But OPTIMIZE ... ZORDER BY is not incremental in the way you would like. Each run rewrites data within the selected partitions, which is expensive on large tables, and the Z-ORDER columns are not stored as a table property. Every job has to remember them.

What liquid clustering is

Liquid clustering stores clustering keys as part of the table definition. Databricks then organises data files so that rows with similar key values sit together, which gives strong file-level data skipping when you filter on those keys. The main differences from the older approach:

  • Incremental: OPTIMIZE only rewrites files that need clustering, not the whole table.
  • Changeable keys: ALTER TABLE ... CLUSTER BY changes the keys without an immediate rewrite of existing data.
  • Up to four keys, which can include high-cardinality columns that would be terrible partition columns.
  • Clustering on write for operations such as INSERT INTO, CTAS and COPY INTO from Parquet, once the write is above a size threshold.

Liquid clustering for Delta tables is generally available on Databricks Runtime 15.4 LTS and above. Note that it cannot be combined with partitioning or Z-ORDER on the same table. It replaces them.

Partitioning vs Z-ORDER vs liquid clustering

AspectPartitioningZ-ORDERLiquid clustering
Defined whereTable definitionEach OPTIMIZE commandTable definition
High-cardinality columnsPoor (small files)GoodGood
Change layout laterFull rewriteRewrite on next runALTER TABLE, recluster when ready
Maintenance costLow, but small-file riskHigh, rewrites a lot of dataIncremental OPTIMIZE
Automatic key choiceNoNoYes, with CLUSTER BY AUTO

Hands-on: creating and maintaining clustered tables

Create a new clustered table

Add CLUSTER BY after the table name. For CTAS, it goes before AS, not inside the SELECT.

-- Empty table with two clustering keys
CREATE TABLE sales.events (
  event_id     BIGINT,
  customer_id  BIGINT,
  event_type   STRING,
  event_ts     TIMESTAMP,
  amount       DECIMAL(12,2)
)
CLUSTER BY (customer_id, event_ts);

-- Create from an existing table (CTAS)
CREATE TABLE sales.events_2026
CLUSTER BY (customer_id, event_ts)
AS SELECT * FROM sales.events_raw
WHERE event_ts >= '2026-01-01';

-- Check which keys a table uses
DESCRIBE DETAIL sales.events;   -- see the clusteringColumns field

In PySpark, the DataFrame and DeltaTable APIs support clustering on Runtime 14.3 LTS and above. Keys can be set when creating the table or in overwrite mode, not in append mode.

(df.write
   .format("delta")
   .clusterBy("customer_id", "event_ts")
   .mode("overwrite")
   .saveAsTable("sales.events"))

Enable clustering on an existing table

For an unpartitioned Delta table, you can switch it on directly. Then run OPTIMIZE FULL (Runtime 16.0+) once to recluster the existing data. On a large table this can take hours, so schedule it off-peak.

-- Unpartitioned table: add clustering keys
ALTER TABLE sales.orders CLUSTER BY (customer_id, order_date);

-- Recluster existing data with the new keys
OPTIMIZE sales.orders FULL;

-- Regular incremental maintenance afterwards
OPTIMIZE sales.orders;

-- Change your mind later: new keys, no immediate rewrite
ALTER TABLE sales.orders CLUSTER BY (order_date, region);

-- Stop clustering future OPTIMIZE runs (existing files untouched)
ALTER TABLE sales.orders CLUSTER BY NONE;

For partitioned tables, Databricks Runtime 18.1 and above adds an in-place conversion. On older runtimes, the usual route is a CTAS or deep clone into a new clustered table.

-- Runtime 18.1+: convert a partitioned table to liquid clustering
ALTER TABLE sales.clicks
  REPLACE PARTITIONED BY WITH CLUSTER BY (click_date, user_id);

-- Runtime 18.1+: recluster only part of the table
OPTIMIZE sales.clicks FULL WHERE click_date >= '2026-01-01';

Let Databricks choose the keys: CLUSTER BY AUTO

For Unity Catalog managed Delta tables on Runtime 15.4 LTS and above, CLUSTER BY AUTO hands key selection to predictive optimization. It looks at your real query history and changes keys only when the predicted data-skipping savings outweigh the cost of reclustering. You can give it a starting point first:

ALTER TABLE sales.events CLUSTER BY (customer_id);  -- initial hint
ALTER TABLE sales.events CLUSTER BY AUTO;           -- let it adapt

With predictive optimization on, Databricks also runs OPTIMIZE for you and recommends turning off your own scheduled OPTIMIZE jobs. Without it, schedule OPTIMIZE yourself. Databricks suggests every one or two hours for tables that are updated often.

Choosing good clustering keys

  • Start from your filters. Pick the columns that appear most often in WHERE clauses and join conditions on large scans.
  • Migrating? Databricks’ own guidance is simple. Old partition columns and old Z-ORDER columns become the clustering keys.
  • Use the original column, such as event_ts, rather than a derived low-cardinality column like event_date that only existed to make partitioning workable.
  • Fewer keys on smaller tables. The limit is four. On tables under about 10 TB, extra keys can slow single-column filters, so two or three is often the sweet spot.
  • Keys need statistics. Delta collects stats on the first 32 columns by default, so a key outside that range needs the stats settings adjusted.

Clustering on write and streaming

Clustering on write only happens when the write is big enough. For Unity Catalog managed tables, the thresholds start at 64 MB for one key and rise to 1 GB for four keys. Other Delta tables have higher thresholds (256 MB to 4 GB). Small, frequent batch writes therefore land unclustered and rely on OPTIMIZE to tidy up later.

For Structured Streaming, creating clustered tables from a stream needs Runtime 16.4 LTS or above. Eager clustering of streaming writes is controlled by spark.databricks.delta.liquid.eagerClustering.streaming.enabled. Even then, it only triggers when at least one of the last five micro-batches is above the size threshold.

Common pitfalls

  • Forgetting OPTIMIZE FULL after changing keys. ALTER TABLE ... CLUSTER BY only affects future clustering. Old files keep the old layout until you recluster.
  • Leaving legacy ZORDER jobs running. Z-ORDER and liquid clustering can’t be mixed. Remove ZORDER BY from maintenance notebooks when you migrate.
  • Clustering on complex types. STRUCT, MAP and ARRAY columns can’t be keys. You can use a nested struct field with dot notation instead.
  • Old readers. Clustered tables use Delta writer version 7 and reader version 3, and the protocol can’t be downgraded. Check external readers (older Spark, Trino or other engines) before enabling clustering on shared tables.
  • Expecting small writes to be clustered. Below the size thresholds, nothing happens on write. Watch file counts and keep maintenance scheduled.
  • Too many keys “just in case”. Each extra key dilutes clustering for the others. Measure with real queries before adding a fourth key.

How to measure the benefit

Run your heaviest queries before and after the migration and compare files read and bytes read in the query profile, not only run time, because warm caches can hide the difference. A well-chosen key on a selective filter should cut the files scanned dramatically. If it doesn’t, check that the filter column really is a clustering key, that it has statistics, and that OPTIMIZE has run since the data landed.

Key takeaways

  • Liquid clustering replaces partitioning and Z-ORDER for most new Delta tables on Databricks. It is GA on Runtime 15.4 LTS and above.
  • Keys live in the table definition, can be changed with ALTER TABLE, and are maintained incrementally by OPTIMIZE.
  • Use OPTIMIZE FULL once after enabling or changing keys, then regular OPTIMIZE or predictive optimization.
  • CLUSTER BY AUTO lets Databricks pick keys from real query patterns on Unity Catalog managed tables.
  • Keep to two or three keys on mid-sized tables, check reader compatibility, and measure files scanned to prove the gain.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your comment appears after we check it.