Thegretia
Back to Articles
Data Engineering
Jan 20, 2025•8 min read

Optimizing PySpark on Delta Lake: Partitioning, Z-Ordering, and Avoiding the Small File Trap

A deep dive into query planning, shuffle optimizations, file compaction strategies, and cost-reduction techniques for multi-terabyte Lakehouse pipelines on Databricks.

Patrick Thomas MBONJO ETIA

Thegretia • Data & AI Engineer

#PySpark#Delta Lake#Databricks#Performance#Big Data

Introduction

When scaling data pipelines on Apache Spark and Delta Lake from gigabytes to terabytes, standard default configurations quickly degrade into performance bottlenecks. High shuffle partitions, memory spills to disk, and the notorious "small file problem" can multiply cloud compute bills by 5x to 10x.

In this technical breakdown, we explore practical engineering strategies implemented in production to maximize data throughput, reduce cluster runtimes, and maintain tight SLAs.


1. Deconstructing the "Small File Problem"

Streaming ingestion (Kafka, IoT telemetry, micro-batches) frequently writes small files (kilobytes in size) directly to cloud storage.

textsnippet
s3://lakehouse/bronze/transactions/
├── part-00000-001.snappy.parquet (45 KB)
├── part-00001-001.snappy.parquet (52 KB)
├── part-00002-001.snappy.parquet (48 KB)
... (100,000+ files)

The Impact on Spark Driver: - **Driver Bottleneck:** The Spark driver spends tremendous CPU time listing files and computing split metadata rather than scheduling tasks. - **Excessive Storage IOPS:** Hundreds of thousands of HTTP GET requests throttle cloud object storage (S3 / ADLS Gen2).

The Solution: Auto-Compaction & Optimized Writes In Delta Lake, enable automatic write optimizations:

pythonsnippet
# Enable auto-optimization at session or table level
spark.conf.set("spark.databricks.delta.optimizeWrite.enabled", "true")

# Or via SQL table properties ALTER TABLE silver_orders SET TBLPROPERTIES ( 'delta.autoOptimize.optimizeWrite' = 'true', 'delta.autoOptimize.autoCompact' = 'true' ); ```


2. Multi-Dimensional Clustering with Z-Ordering

Standard directory partitioning (e.g. PARTITIONED BY date, region) is effective for high-cardinality time series, but over-partitioning creates sub-directories with minute files.

Z-Order curves solve this by co-locating related information within the same physical files without altering directory structures:

sqlsnippet
-- Run regular maintenance optimization
OPTIMIZE silver_customer_events
ZORDER BY (customer_id, event_type);

Why it works: Delta Lake stores min/max statistics for each column in every Parquet file in the _delta_log. When downstream queries filter by WHERE customer_id = 'CUST-84920', Spark immediately skips 90%+ of irrelevant Parquet files (Data Skipping), turning a 45-second table scan into a 1.2-second predicate lookup.


3. Shuffle Optimization & Adaptive Query Execution (AQE)

In Apache Spark 3.x+, Adaptive Query Execution dynamically tunes query plans at runtime based on actual stage statistics.

pythonsnippet
# Critical AQE configurations for heavy transformations
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728") # 128MB target

Handling Data Skew: When a join key has a skewed distribution (e.g. 50% of orders come from a default customer ID), one worker task drags down the entire job stage. spark.sql.adaptive.skewJoin.enabled automatically detects skewed partitions and splits them into smaller sub-tasks.


Key Takeaways 1. Always maintain Parquet file sizes between 128MB and 1GB using scheduled OPTIMIZE jobs. 2. Replace over-partitioning with Z-Order indexing on high-cardinality filter columns. 3. Turn on Adaptive Query Execution to automate partition coalescing and skew resolution.