← Back to list

Databricks Data Layout Explained: Partitioning, OPTIMIZE, Z-ORDER & Liquid Clustering

If you’ve worked with Databricks, you’ve probably heard these four terms: Partitioning. OPTIMIZE. Z-ORDER. Liquid Clustering.

Ayushjoshi · 2026-08-19 21:55 · 1 claps · 12.1 min read
#databricks #partitioning #liquid-clustering #databricks-optimization #data-engineering
Open on Medium ↗
Wiki topics: 🔧 · Data Engineering

Databricks Data Layout Explained: Partitioning, OPTIMIZE, Z-ORDER & Liquid Clustering

If you’ve worked with Databricks, you’ve probably heard these four terms: Partitioning. OPTIMIZE. Z-ORDER. Liquid Clustering.

They are often discussed together, but they solve different problems.The easiest way to understand them is to start with one question:

How can Databricks avoid reading unnecessary Parquet files when I query a Delta table?

This article builds that understanding from the ground up.

1. First: What does a Delta table actually contain?

A Delta table is essentially:

sales/
│
├── _delta_log/
│
├── part-001.parquet
├── part-002.parquet
├── part-003.parquet
└── ...

The Parquet files contain the actual data.

The _delta_log keeps track of which files belong to the current table version, which files were added/removed, schema changes, transactions, etc.

This is important because Databricks doesn’t normally modify an existing Parquet file when new data arrives.

For example:

BEFORE INSERT
A.parquet
B.parquet
C.parquet
   +
new data
---------------------------------------
AFTER INSERT
A.parquet
B.parquet
C.parquet
D.parquet   ← new file

The existing files are generally not opened and modified just to insert new rows.

2. The real performance goal: Read fewer files

Suppose a table has:

100 GB
1000 Parquet files

And you run:

SELECT *
FROM sales
WHERE customer_id = 123;

The ideal situation is:

1000 files
    ↓
Eliminate irrelevant files
    ↓
Read only 5 files

Databricks achieves this using techniques such as:

Partition pruning
       +
Data skipping
       +
Good physical data layout

This is where Partitioning, Z-ORDER and Liquid Clustering come in.

3. Partitioning: Separate data physically

Suppose we create:

CREATE TABLE sales
USING DELTA
PARTITIONED BY (order_date);

The physical layout can look like:

sales/
│
├── order_date=2026-08-18/
│   ├── part-001.parquet
│   └── part-002.parquet
│
└── order_date=2026-08-19/
    ├── part-003.parquet
    └── part-004.parquet

The important point is:

Partitioning creates a physical separation of data based on partition-column values.

Now consider:

SELECT *
FROM sales
WHERE order_date = '2026-08-19';

Databricks can perform partition pruning:

order_date=2026-08-18  → SKIP
order_date=2026-08-19  → READ

Instead of considering files from every date.

When is partitioning useful?

Traditionally, partitioning works well when:

  • The table is sufficiently large.
  • The partition column is frequently used in filters.
  • The column has relatively low/moderate cardinality.
  • Each partition contains a reasonable amount of data.

Typical examples:

order_date
country
region
year/month

Be careful with high-cardinality columns such as:

customer_id
transaction_id

Partitioning by millions of unique customer IDs can create huge numbers of partitions and small files.

4. The small-file problem

Suppose a streaming or frequent ingestion process keeps appending data:

part-001.parquet → 5 MB
part-002.parquet → 8 MB
part-003.parquet → 3 MB
part-004.parquet → 7 MB
...

Eventually you might have thousands of small files.

Even if the table contains only 100 GB, managing thousands of files can hurt performance.

100 GB table

Option A:
100 large files
       ↓
Fewer files to manage/read

Option B:
10,000 small files
       ↓
Much more file-management overhead
       ↓
More metadata operations
       ↓
More task scheduling overhead
       ↓
Potentially slower queries

This is where OPTIMIZE comes in.

5. OPTIMIZE: Rewrite files into a better layout

Run:

OPTIMIZE sales;

Conceptually:

           BEFORE
A.parquet → 5 MB
B.parquet → 8 MB
C.parquet → 7 MB
D.parquet → 4 MB
E.parquet → 9 MB
             ↓
          OPTIMIZE
             ↓
           AFTER
A'.parquet → larger optimized file
B'.parquet → larger optimized file

OPTIMIZE reads suitable existing files and rewrites them into a better physical layout.

It can help with:

  • Small-file problems
  • File sizing
  • Data layout
  • Z-ORDER
  • Liquid clustering

6. Does OPTIMIZE delete the old Parquet files?

Not immediately.

Suppose:

BEFORE

A.parquet
B.parquet
C.parquet
D.parquet

After OPTIMIZE, the Delta transaction log can conceptually say:

REMOVE:
A.parquet
B.parquet
C.parquet
D.parquet

ADD:
A'.parquet
B'.parquet

The old files can still physically exist because Delta may need them for older table versions/time travel.

Later, VACUUM can remove obsolete physical files according to retention rules.

So remember:

OPTIMIZE
   ↓
Logically removes old files
   ↓
Creates new optimized files

==========================================

VACUUM
   ↓
Physically deletes obsolete files

OPTIMIZE ≠ VACUUM

7. What is Data Skipping?

Data skipping is one of the most important concepts behind all of this.

Suppose:

file-A.parquet

customer_id:
1 → 1000

Databricks can have statistics such as:

min = 1
max = 1000

Another file:

file-B.parquet

customer_id:
1001 → 2000

Now query:

SELECT *
FROM sales
WHERE customer_id = 1500;

Databricks can reason:

file-A: 1500 is outside 1-1000
        → SKIP
file-B: 1500 is inside 1001-2000
        → READ

This is data skipping.

So:

Good physical layout
       ↓
Better file statistics
       ↓
More effective data skipping
       ↓
Fewer files read
       ↓
Better query performance

8. Z-ORDER: Improve data locality

Now imagine the data is badly distributed.

You might have:

file-A:
customer_id = 1, 50000, 900000, 20...

file-B:
customer_id = 100, 800000, 3000...

file-C:
customer_id = 500000, 50, 9000000...

The min/max range of each file can become extremely wide.

Data skipping becomes less effective.

This is where Z-ORDER can help.

OPTIMIZE sales
ZORDER BY (customer_id);

Z-ORDER reorganizes data so that related values are more likely to be colocated in the same files.

Conceptually:


BEFORE Z-ORDER (random customer IDs)
────────────────────────────────────────

file-A.parquet
→ customer_id: 5, 900, 45, 700, 12

file-B.parquet
→ customer_id: 300, 20, 850, 150, 600

file-C.parquet
→ customer_id: 80, 500, 10, 950, 250

AFTER Z-ORDER BY (customer_id)
────────────────────────────────────────

file-A.parquet
→ customer_id: 5, 10, 12, 20, 45

file-B.parquet
→ customer_id: 80, 150, 250, 300, 500

file-C.parquet
→ customer_id: 600, 700, 850, 900, 950

The exact ranges are not guaranteed; this is a simplified mental model.

The real objective is:

Better data locality → tighter file statistics → better data skipping.

8a. Z-ORDER rewrites data fully, not incrementally

Unlike liquid clustering, Z-ORDER does not selectively touch only the “unorganized” portion of your data. When you run:

OPTIMIZE sales
ZORDER BY (customer_id);

Databricks takes the files being optimized and rewrites all of them from scratch, based on the current values of customer_id across the whole set. There's no concept of "this file is already well-ordered, skip it" — every file that's part of the operation gets reshuffled and rewritten.

100 GB table
       ↓
OPTIMIZE + ZORDER
       ↓
Entire 100 GB re-evaluated and rewritten
       ↓
New optimized files

This matters for two reasons:

Cost. Each time you run ZORDER BY, you're paying the compute cost of rewriting the full dataset (or full partition, if partitioned) again — not just the new data since the last run.

Run 1: OPTIMIZE ZORDER BY (customer_id)  → rewrites 100 GB
+10 GB new data arrives
Run 2: OPTIMIZE ZORDER BY (customer_id)  → rewrites full ~110 GB again

No memory of prior work. Z-ORDER has no awareness that most of the table was already well-clustered from the last run. It re-derives the layout from zero every time, which is simple and predictable, but doesn’t scale as gracefully as liquid clustering’s incremental approach.

This is one of the core reasons liquid clustering scales better on large, frequently-updated tables — Z-ORDER’s cost grows with total table size on every optimization pass, while liquid clustering’s cost is meant to grow with only the portion that actually needs work.

9. Partitioning + Z-ORDER

Partitioning and Z-ORDER can complement each other:

  • Partitioning helps Databricks identify the relevant partition.
  • Z-ORDER improves data locality within that partition, making data skipping more effective.

Suppose we create a table partitioned by order_date:

CREATE TABLE sales
USING DELTA
PARTITIONED BY (order_date);

Then:

OPTIMIZE sales
ZORDER BY (customer_id);

A simplified physical layout could look like:

sales/
│
├── order_date=2026-08-18/
│   ├── files...
│
└── order_date=2026-08-19/
    ├── file-A.parquet
    ├── file-B.parquet
    └── file-C.parquet

Inside the 2026-08-19 partition, Z-ORDER improves the locality of customer_id values.

So for a query like:

SELECT *
FROM sales
WHERE order_date = '2026-08-19'
  AND customer_id = 20;

the query can benefit from:

Query
  │
  ├── order_date = 2026-08-19
  │          ↓
  │    Partition pruning
  │          ↓
  │    Ignore other partitions
  │
  └── customer_id = 20
             ↓
       Data skipping
             ↓
       Ignore files that
       cannot contain 20

The overall idea is:

Partitioning
    ↓
Find the right partition

Z-ORDER
    ↓
Find the right files within it

    ↓
Read less data

Partitioning narrows the search to the right partition; Z-ORDER helps narrow it further to the relevant files.

10. What happens when new data arrives?

This is one of the most misunderstood parts. Suppose the 2026-08-19 partition currently contains:

order_date=2026-08-19/
  file-A.parquet → customer IDs around 1-33
  file-B.parquet → customer IDs around 34-66
  file-C.parquet → customer IDs around 67-100

Now new data arrives containing customers 1-33.

The existing files are not normally modified just to accommodate the new rows. Instead, the new data is written as a new Parquet file:

order_date=2026-08-19/
    file-A.parquet → customer IDs around 1-33
    file-B.parquet → customer IDs around 34-66
    file-C.parquet → customer IDs around 67-100
    file-D.parquet → customer IDs around 1-33   ← NEW

Notice that file-A and file-D now contain similar customer IDs. This is perfectly valid. The table is immediately queryable.

For:

SELECT *
FROM sales
WHERE order_date = '2026-08-19'
  AND customer_id = 20;

Databricks can still use the available file statistics:

file-A → possible → READ
file-B → impossible → SKIP
file-C → impossible → SKIP
file-D → possible → READ

11. What happens when OPTIMIZE runs?

At some point, we may want to improve the physical layout again:

OPTIMIZE sales
ZORDER BY (customer_id);

Databricks can identify suitable files and rewrite them into a better physical layout.

Conceptually:

BEFORE OPTIMIZE
────────────────────────────────
order_date=2026-08-19/
file-A → customer IDs around 1-33
file-B → customer IDs around 34-66
file-C → customer IDs around 67-100
file-D → customer IDs around 1-33   ← newly appended

│
│ OPTIMIZE
│ + Z-ORDER
▼

AFTER OPTIMIZE
────────────────────────────────
order_date=2026-08-19/
file-A' → optimized customer-ID locality
file-B' → optimized customer-ID locality
file-C' → optimized customer-ID locality

The old files are logically removed from the current Delta table version and the new optimized files become active.

Conceptually:

Delta transaction log

REMOVE:
file-A
file-B
file-C
file-D
ADD:
file-A'
file-B'
file-C'

The exact number, size, and contents of the resulting files are decided by Databricks. It is not guaranteed that 4 files will always become exactly 3 files.

The important thing is:

New data arrives
↓
New Parquet file(s)
↓
Table immediately queryable
↓
OPTIMIZE
↓
Files are rewritten/reorganized
↓
Better physical layout
↓
Potentially better data skipping
↓
Better query performance

OPTIMIZE is a maintenance operation. It is not required for the INSERT to be correct or for the new data to be queryable.

12. What happens between INSERT and OPTIMIZE?

This is critical. Suppose:

10:00 → INSERT
10:01 → SELECT
10:10 → OPTIMIZE

The 10:01 query can already see the newly committed data.

You do not need to wait for OPTIMIZE.

However, the physical layout may be temporarily less optimal.

Think:

INSERT
  ↓
New files
  ↓
Immediately queryable
  ↓
Possibly less-optimal layout
  ↓
OPTIMIZE
  ↓
Better physical layout

So OPTIMIZE affects performance/physical organization, not correctness.

13. Now Liquid Clustering

So far, we’ve seen two ways to organize data: partitioning (physical folders) and Z-ORDER (a one-time reorganization command). Liquid clustering is a third approach — and it’s meant to replace both in most new tables.

Create a table with it like this:

CREATE TABLE sales
USING DELTA
CLUSTER BY (customer_id);

The most important thing to understand upfront is what it doesn’t do. It does not create folders like this:

customer_id=1/
customer_id=2/
customer_id=3/

That’s what partitioning does. Liquid clustering never creates a physical folder per key value.

Instead, it works at the file level. Databricks keeps track of a “clustering key” (here, customer_id) and continuously tries to group rows with similar key values into the same Parquet files — without you ever running a manual ZORDER BY command.

file-A → customer IDs in one locality
file-B → customer IDs in another locality
file-C → customer IDs in another locality

The goal is identical to Z-ORDER’s goal — better locality, so file statistics become tighter and data skipping becomes more effective. What’s different is how that locality gets maintained.

14. Why is Liquid Clustering different?

To see why this matters, compare it to partitioning’s core weakness: fixed boundaries.

country=India/
country=USA/
country=UK/

Once you partition by country, that boundary is baked into the table's physical layout. If your query patterns shift, or the data distribution changes, the folders don't reshape themselves — you're stuck with the layout you chose at creation time, or you have to rewrite the table.

Liquid clustering avoids this by never committing to fixed physical boundaries in the first place. The clustering key is just a strategy, and that strategy can change:

ALTER TABLE sales
CLUSTER BY (product_id);CLUSTER BY (customer_id);

After this, future optimization passes will progressively reorganize the table around the new key — product_id instead of customer_id — without needing to drop and recreate anything. This flexibility is a big part of why Databricks now recommends liquid clustering as the default choice for most new tables, instead of partitioning or Z-ORDER.

15. What happens when new data arrives in a Liquid Clustered table?

Just like with partitioned or Z-ORDERed tables, new data doesn’t get force-merged into the existing layout the moment it arrives. Say we already have:

A.parquet
B.parquet
C.parquet

and append:

D.parquet ← new data

D.parquet simply lands as its own file. It is not automatically rewritten into A, B, or C just because those files were already well-clustered.

A
B
C
D ← new

The table is fully valid and queryable right away, even though D.parquet hasn't been "absorbed" into the clustered layout yet. That absorption is a separate, later step — clustering maintenance — not something that happens synchronously on every insert.

16. What does incremental reorganization mean?

This is the part that makes liquid clustering practical at scale. Imagine your table has:

100 GB existing data
+
10 GB new data

The naive way to keep everything perfectly clustered would be to rewrite the whole 110 GB every time anything changes. That’s obviously expensive, and it gets worse as the table grows.

Liquid clustering avoids this by only touching the parts of the table that actually need it:

110 GB
│
├── Already well organized → leave alone
│
├── New data              → evaluate
│
└── Data needing work     → reorganize

Databricks decides internally how much data actually needs to be rewritten — it’s not necessarily just the 10 GB of new data, and it’s not necessarily the full 110 GB either. The point isn’t the exact amount; it’s the principle: don’t rebuild the entire table when only a fraction of it is out of shape. This incremental behavior is what lets liquid clustering scale to very large tables without turning every optimization pass into a full table rewrite.

17. Does Liquid Clustering automatically reorganize on every INSERT?

No.

This is an important distinction.

Don’t think:

INSERT
  ↓
Immediately reorganize entire table

Think:

CLUSTER BY
    ↓
Defines desired physical layout
INSERT
    ↓
New Parquet files
OPTIMIZE / automated optimization
    ↓
Incremental physical reorganization

For supported Unity Catalog managed tables, predictive optimization can automate maintenance operations such as OPTIMIZE when appropriate.

18. Z-ORDER vs Liquid Clustering

Both techniques exist to solve the exact same problem — poor data locality leading to weak data skipping. But they differ in who is responsible for keeping that locality good over time, and that difference matters a lot in practice.

Z-ORDER is a command you run, not a property of the table.

OPTIMIZE sales
ZORDER BY (customer_id);

Nothing about the table “remembers” that it should stay Z-ORDERed by customer_id. It's just an instruction: "reorganize the files that exist right now, using this column." The moment new data lands, that new data is unordered again — and it stays that way until someone runs OPTIMIZE ... ZORDER BY a second time. This means you, or a scheduled job, has to keep deciding:

Which columns should I Z-ORDER on?
How often should this run?
Is the same column still the right choice as query patterns change?

If nobody re-runs it, the layout slowly degrades back toward randomness, and data skipping quietly gets worse.

Liquid clustering is a property you declare on the table itself.

CREATE TABLE sales
CLUSTER BY (customer_id);

Here, customer_id isn't a one-off instruction — it's a standing definition of how the table wants to be organized. Databricks can maintain that layout incrementally and automatically as data changes, rather than waiting for you to manually trigger a rewrite. You don't need to remember to run ZORDER BY customer_id every time you optimize; the clustering key already tells Databricks what "well organized" means for this table.

The practical difference:

Both ultimately aim at the same outcome — tighter file statistics, better skipping — but Z-ORDER asks you to actively drive that outcome, while liquid clustering asks you to declare the goal once and let the table maintain itself toward it.

19. Why not partition by customer_id?

Suppose:

customer_id = 10 million unique values

Partitioning could conceptually create:

customer_id=1/
customer_id=2/
customer_id=3/
...

This can result in huge numbers of tiny partitions/files.

That’s a classic high-cardinality partitioning problem.

Liquid clustering can instead use:

CLUSTER BY (customer_id);

without creating one filesystem partition per customer.

20. A complete comparison

21. The most important mental model

If you remember only this:

                DELTA TABLE
                     │
                     ▼
              Parquet files
                     │
       ┌─────────────┼──────────────┐
       │             │              │
       ▼             ▼              ▼
 Partitioning    Z-ORDER     Liquid Clustering
       │             │              │
       ▼             ▼              ▼
  Separate       Improve        Flexible/
  directories    locality       incremental
       │             │           locality
       └─────────────┼──────────────┘
                     ▼
              Data skipping
                     │
                     ▼
             Read fewer files
                     │
                     ▼
              Faster queries

And:

OPTIMIZE
   ↓
Actually performs physical file
maintenance/reorganization

Thanks for reading! I’m Ayush. If this helped you understand data engineering a little better, I’ve done my job. More coming soon🚀


메타데이터
post_id
db6cd2ed2ce0
slug
databricks-data-layout-explained-partitioning-optimize-z-order-liquid-clustering-db6cd2ed2ce0
url
https://medium.com/@ayushjoshi050/databricks-data-layout-explained-partitioning-optimize-z-order-liquid-clustering-db6cd2ed2ce0
canonical_url
https://medium.com/@ayushjoshi050/databricks-data-layout-explained-partitioning-optimize-z-order-liquid-clustering-db6cd2ed2ce0
author_url
https://medium.com/@ayushjoshi050
status
ok
fetched_at
2026-08-21 17:08:42