Optimize Delta Tables with Z-Ordering
Learn how to optimize Delta tables with Z-ordering in Databricks. This hands-on tutorial covers the core concept, step-by-step implementation, and best practices to improve query performance on large datasets.
Focus: optimize delta tables with z-ordering
You've built your Delta tables, loaded your data, and maybe even run a few queries. But as your dataset grows to millions or billions of rows, you notice that queries that used to be snappy are now crawling. The culprit? Data sprawled across files with no logical organization. This is the exact pain point that Z-ordering solves — a clustering technique that can dramatically speed up data skipping and reduce query latencies on large Delta tables. In this lesson, you'll learn what Z-ordering is, how to apply it step by step, and when it's the right tool versus alternatives like OPTIMIZE alone or partitioning.
The problem this lesson solves
Imagine you have a Delta table with a billion rows of sales data. Every time you filter on customer_id or order_date, Spark has to scan all the underlying files to find matching rows. That's like searching for a needle in a haystack where the hay is scattered across a warehouse. Without a strategy to organize data, your queries are slow, your cluster bills are high, and your users are impatient.
The default layout of a Delta table is append-only: new data lands in whatever files are available. No attempt is made to group related data together. This works fine for small tables, but as you scale, the lack of clustering becomes a performance bottleneck. The need to optimize Delta tables with Z-ordering arises when you want to skip irrelevant files during queries, drastically reducing the amount of data read.
Core concept / mental model
Think of a library. Without any organization, finding a book on, say, Python programming means walking every aisle and checking every shelf. But if we organize books by subject, we know exactly which section to go to — we can skip entire aisles. Z-ordering is like adding a multi-dimensional index to your data files. It sorts data values along multiple columns into a space-filling curve that keeps similar values close together in physical storage.
The Z-order curve (named after the 'Z' shape it forms when plotted) is a way to map multi-dimensional data to a 1-dimensional ordering. When applied to a Delta table, it ensures that rows with similar values for the specified columns are stored in the same or nearby files. This enables data skipping, where the query engine reads only the file metadata (like min/max statistics) and skips files that don't contain relevant data.
Pro tip: Z-ordering is not indexing in the traditional RDBMS sense. It doesn't create an index structure; it rearranges the physical layout so that the existing I/O pruning can work more effectively.
How it works step by step
-
Identify the Z-order columns. Choose columns that are frequently used in filters (e.g.,
WHEREclauses) and have high cardinality (many distinct values). Typical candidates include high-volume dimensions likecustomer_id,product_id, or date timestamps. -
Run the
OPTIMIZEcommand with Z-ordering. In Databricks, you useOPTIMIZE table_name ZORDER BY (column1, column2). This command rewrites data files into a clustered layout based on the Z-order curve of those columns. Note that the order of columns matters: the first column has the most influence on clustering. -
Wait for the job to complete. The operation is a rewrite of existing data files, so it can be resource-intensive. Databricks automatically determines how many files to split into to achieve the desired clustering. You can also set a
WHEREclause to optimize only a subset of data (e.g., recent partitions). -
Verify the impact. After optimization, check query performance on filters that use the Z-order columns. Data skipping should be evident in the query plan or by observing the number of files scanned.
Hands-on walkthrough
Let's get practical. We'll create a sample Delta table, load some data, run a query, then apply Z-ordering and run the same query to see the difference in file scanning.
First, create a DataFrame and write it as a Delta table:
from pyspark.sql import SparkSession
from pyspark.sql.functions import rand, col, date_add, lit
spark = SparkSession.builder.getOrCreate()
# Create sample sales data: 1 million rows with customer_id, product_id, order_date
df = spark.range(1_000_000) \
.withColumn("customer_id", (rand() * 100000).cast("int")) \
.withColumn("product_id", (rand() * 1000).cast("int")) \
.withColumn("order_date", date_add(lit("2023-01-01"), (rand() * 365).cast("int")))
df.write.format("delta").save("/tmp/sales_table")
Now create a Delta table from that location:
spark.sql("CREATE TABLE sales USING DELTA LOCATION '/tmp/sales_table'")
Let's see how many files are initially and run a filter query:
df_sales = spark.table("sales")
print("Initial file count:", df_sales.inputFiles().length)
# Query: filter on customer_id and count
start = time.time()
result = df_sales.filter(col("customer_id") == 12345).count()
print(f"Query count: {result}, Time: {time.time() - start:.2f}s")
Now optimize with Z-ordering on customer_id and order_date:
spark.sql("OPTIMIZE sales ZORDER BY (customer_id, order_date)")
Check the file count again and run the same query:
print("After optimize file count:", df_sales.inputFiles().length)
start = time.time()
result = df_sales.filter(col("customer_id") == 12345).count()
print(f"Query count after Z-order: {result}, Time: {time.time() - start:.2f}s")
Expected output: You'll likely see a dramatic reduction in the number of files scanned (from many to a few) and a faster query time. The exact numbers vary, but the trend is clear.
Pro tip: To see how many files are actually scanned, use
df_sales.filter(col("customer_id") == 12345).explain()and look at thePartitionFiltersandFileScannode. You'll see that only a subset of files is read after Z-ordering.
Compare options / when to choose what
Both OPTIMIZE (bin-packing) and Z-ordering are optimization techniques, but they serve different purposes. Z-ordering is more powerful for selective queries, but it's more expensive. Here's a quick comparison:
| Technique | What it does | Best for | Cost |
|---|---|---|---|
| OPTIMIZE (bin-packing) | Merges small files into larger files, reducing file count | Tables with many small files, improving general scan throughput | Medium (one-time rewrite) |
| Z-ORDER BY | Clusters data by specified columns within files, enabling data skipping | Queries with filters on high-cardinality columns | High (more intensive than bin-packing) |
| Partitioning | Divides table into subdirectories by a column (e.g., date) | Query patterns that always filter on the partition column | Low (during writes) but can create too many partitions |
In practice, you might combine approaches: partition on a low-cardinality dimension (like year), then Z-order on a high-cardinality one (like customer_id). Z-ordering works best on columns that are not already partition columns.
Pro tip: Don't Z-order on a column with low cardinality (like
statuswith only 3 distinct values) — it won't help and will waste resources. Always profile your query workload to pick Z-order columns.
Troubleshooting & edge cases
- Too many Z-order columns: Z-ordering on 3 or more columns can degrade performance because the data becomes too scattered. Stick to 1–2 columns, or maybe 3 if they are carefully chosen.
- Z-ordering on partition columns: Avoid Z-ordering on columns that are already used for partitioning. The partition pruning already handles them, and Z-ordering will not add value.
- High cost with large tables: Running
OPTIMIZE ... ZORDER BYon a huge table can be expensive. Break it into increments by usingWHEREto optimize recent partitions first. - No skill in data skipping: If queries are still slow, check if
delta.dataSkippingNumIndexedColsis set correctly. By default, only the first 32 columns are indexed for statistics. If your Z-order column is beyond that, data skipping won't work. - Z-ordering does not speed up all queries: It only helps when predicates match the Z-order columns. A query on column
product_idwon't benefit if you Z-ordered oncustomer_idandorder_date. - Queries on small tables: For tables under a few hundred MB, the overhead might not justify the optimization. Use them when file size or scan time is significant.
What you learned & what's next
You now understand how to optimize Delta tables with Z-ordering — the core idea of clustering data to enable data skipping, the step-by-step way to apply it with OPTIMIZE ... ZORDER BY, and when it's better than plain OPTIMIZE or partitioning. You also learned troubleshooting tips to avoid common pitfalls like picking wrong columns or over-optimizing.
Next up in the Databricks track: you'll likely explore delta table vacuuming or time travel. But before you move on, practice what you've learned: take a large table you have and apply Z-ordering based on your most frequent query filters. Measure the improvement and notice the file-scan reduction in the query plan.
Remember: Z-ordering is your friend for selective queries, but use it wisely — a little clustering goes a long way.
Practice recap
Create a Delta table from the /tmp/sales_table data, run a couple of queries with filters on customer_id and order_date, then OPTIMIZE with Z-ordering on those columns. Re-run the queries and use explain() to observe the reduced file scans. Experiment with ordering of columns and compare performance.
Common mistakes
- Choosing Z-order columns with low cardinality (e.g., a boolean flag) — it won't help and wastes resources.
- Z-ordering on more than 2-3 columns, which scatters data and hurts query performance.
- Forgetting to check whether the Z-order column is already a partition column; it negates the benefit.
- Skipping the 'delta.dataSkippingNumIndexedCols' setting issue, so columns beyond 32 aren't indexed and data skipping doesn't happen.
- Running Z-ordering on a small table where the optimization overhead outweighs the benefits.
Variations
- Use
OPTIMIZE table ZORDER BY (col1)with aWHEREclause to optimize only a partition or a subset of data, reducing job cost. - Combine Z-ordering with liquid clustering, a newer feature that offers incremental clustering without a full rewrite (available in Databricks Runtime 13.3+).
- Alternate approach: use file pruning via
OPTIMIZEbin-packing only, if your queries are not selective enough to benefit from Z-ordering.
Real-world use cases
- A retail analytics platform that frequently queries sales by
customer_iduses Z-ordering to reduce scan time from minutes to seconds on billions of rows. - A financial services company runs daily ETL and queries on
account_idandtransaction_date; Z-ordering on these columns improves compliance reporting performance. - A SaaS company with a large event log table filters by
user_idandevent_date; Z-ordering on these columns enables interactive dashboards without a full scan.
Key takeaways
- Z-ordering is a physical data organization technique that enables data skipping to speed up filter queries on Delta tables.
- Use
OPTIMIZE table ZORDER BY (col1, col2)to cluster data by frequently filtered columns. - Choose 1-3 high-cardinality columns that are not already partition columns for best results.
- Better than plain
OPTIMIZEfor selective queries; combine with partitioning for even better performance. - Check your query plan and file scan count to verify the improvement after Z-ordering.
- Z-ordering is resource-intensive; consider incremental optimization for large tables.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.