Datanest Digital — Spark Optimization Playbook
AQE is Spark's runtime query re-optimization engine. It observes actual data statistics during execution and adjusts the query plan on the fly. This guide covers every AQE feature, when it helps, and how to tune it.
AQE re-optimizes the query plan at stage boundaries (after each shuffle). It can:
1. Coalesce shuffle partitions — Merge small post-shuffle partitions into larger ones
2. Switch join strategies — Convert sort-merge join to broadcast join at runtime
3. Handle skewed joins — Split skewed partitions and replicate the other side
4. Optimize skewed aggregations — Split skewed groups across multiple tasks
# Master switch (enabled by default on DBR 12.2+)
spark.conf.set("spark.sql.adaptive.enabled", "true")AQE is generally safe to enable and should be on for all workloads. The only reason to disable it is for benchmarking to isolate its impact.
After a shuffle, some partitions may be tiny (a few KB). AQE merges adjacent small partitions into larger ones to reduce task overhead.
| Config | Default | Recommended | Notes |
|---|---|---|---|
spark.sql.adaptive.coalescePartitions.enabled | true | true | Master switch for coalescing |
spark.sql.adaptive.coalescePartitions.initialPartitionNum | (none) | 4096 | Starting shuffle partitions before coalescing. Higher = more granularity for AQE to work with |
spark.sql.adaptive.coalescePartitions.minPartitionSize | 1 MB | 4-16 MB | Minimum size per coalesced partition |
spark.sql.adaptive.advisoryPartitionSizeInBytes | 64 MB | 128 MB | Target partition size after coalescing |
# For large datasets (> 100 GB after shuffle):
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.initialPartitionNum", "4096")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728") # 128 MB
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionSize", "4194304") # 4 MB
# For small datasets (< 10 GB after shuffle):
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "67108864") # 64 MB
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionSize", "1048576") # 1 MBinitialPartitionNumdata_size_bytes / advisoryPartitionSizeInBytes * 3When AQE discovers that one side of a sort-merge join is small enough to broadcast (below the broadcast threshold), it dynamically converts the join to a broadcast hash join — eliminating the shuffle on the large side.
| Config | Default | Recommended | Notes |
|---|---|---|---|
spark.sql.adaptive.autoBroadcastJoinThreshold | = autoBroadcastJoinThreshold | 100-256 MB | Runtime threshold for converting to broadcast. Can be higher than the static threshold because AQE uses actual sizes |
spark.sql.autoBroadcastJoinThreshold | 10 MB | 100 MB | Static planning threshold (pre-AQE). AQE can override this at runtime |
# Set static threshold conservatively
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "104857600") # 100 MB
# AQE will additionally convert joins at runtime when it discovers
# actual table sizes — no additional config needed beyond enabling AQEDetects partitions that are significantly larger than the median. Splits the skewed partition into sub-partitions and replicates the corresponding partition from the other side of the join.
| Config | Default | Recommended | Notes |
|---|---|---|---|
spark.sql.adaptive.skewJoin.enabled | true | true | Master switch |
spark.sql.adaptive.skewJoin.skewedPartitionFactor | 5 | 5-10 | A partition is skewed if size > median * factor |
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes | 256 MB | 256 MB | Minimum absolute size to be considered skewed |
# Default settings work well for most cases
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
# For highly skewed data (e.g., 100:1 skew ratio):
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "134217728") # 128 MB
# For moderate skew (10:1 ratio), tighten the detection:
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "3")For GROUP BY operations with skewed groups, AQE can split the aggregation into two phases: a partial aggregation on sub-partitions, then a final merge.
| Config | Default | Notes |
|---|---|---|
spark.sql.adaptive.optimizeSkewedJoin.enabled | true | Databricks-specific extension |
This is generally handled automatically when AQE is enabled.
Copy-paste this block as a starting point for your cluster init scripts or notebook headers:
# --- AQE Configuration — Datanest Digital Recommended ---
# Master switch
spark.conf.set("spark.sql.adaptive.enabled", "true")
# Partition coalescing
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.initialPartitionNum", "4096")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728") # 128 MB
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionSize", "4194304") # 4 MB
# Join optimization
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "104857600") # 100 MB
# Skew handling
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "268435456") # 256 MB1. Open the query's SQL plan in the Spark UI
2. Look for AdaptiveSparkPlan as the root node
3. Look for annotations:
coalesced partitions: X -> Y (coalescing happened)BroadcastHashJoin where you expected SortMergeJoin (dynamic switching)skewed annotations on join nodes (skew handling)# Check if AQE is active
print(spark.conf.get("spark.sql.adaptive.enabled"))
# After running a query, check the plan
df = spark.table("my_table").groupBy("key").count()
df.explain(mode="formatted")
# Look for "AdaptiveSparkPlan" in the output1. Setting shuffle.partitions too low with AQE — AQE can only coalesce (merge) partitions, not split them. Set initialPartitionNum high.
2. Disabling coalescing but keeping AQE — You lose the most impactful feature.
3. Expecting AQE to fix all skew — AQE skew handling is limited to sort-merge joins. Use salting for complex skew patterns.
4. Ignoring AQE overhead — AQE adds milliseconds of planning overhead per stage. For sub-second queries, this can be significant. Disable for very fast queries.
*AQE Tuning Guide — Datanest Digital Spark Optimization Playbook*
*https://datanest.dev*
Datanest Digital — Spark Optimization Playbook
A decision-tree approach to selecting the right Databricks cluster configuration based on your workload characteristics, data volume, and budget constraints.
1. Right-size, don't over-provision. Larger clusters cost more per DBU-hour and often hit diminishing returns.
2. Match instance type to workload. Compute-optimized vs. memory-optimized vs. storage-optimized matters.
3. Use autoscaling for variable workloads. Set min/max workers based on peak and trough.
4. Start small, measure, adjust. Use the benchmarking framework to validate sizing decisions.
What is your PRIMARY workload?
│
├── ETL / Data Transformation
│ ├── Shuffle-heavy (joins, groupBy, window functions)?
│ │ ├── YES → Memory-optimized (r5d, Standard_E series, n2-highmem)
│ │ └── NO → General purpose (m5d, Standard_DS, n2-standard)
│ │
│ └── Large intermediate datasets that need local disk?
│ ├── YES → Storage-optimized (i3, Standard_L series)
│ └── NO → General purpose
│
├── Machine Learning Training
│ ├── GPU required?
│ │ ├── YES → GPU instances (p3, Standard_NC, a2-highgpu)
│ │ └── NO → Compute-optimized (c5, Standard_F series, c2-standard)
│ │
│ └── Large model / embeddings in memory?
│ ├── YES → Memory-optimized
│ └── NO → Compute-optimized
│
├── SQL Analytics / BI Queries
│ ├── Concurrent users > 10?
│ │ ├── YES → SQL Warehouse (serverless recommended)
│ │ └── NO → SQL Warehouse (classic) or Photon-enabled cluster
│ │
│ └── Queries scan > 1 TB?
│ ├── YES → Storage-optimized with Photon
│ └── NO → General purpose with Photon
│
└── Streaming
├── Micro-batch latency < 1 minute?
│ ├── YES → Fixed-size cluster (no autoscaling), memory-optimized
│ └── NO → Autoscaling cluster, general purpose
│
└── State management (aggregations, dedup)?
├── YES → Memory-optimized, size for state
└── NO → General purpose
Use these as starting points. Adjust based on benchmarking results.
| Data Volume (Input) | Suggested Workers | Instance Size | Notes |
|---|---|---|---|
| < 10 GB | 2-4 | xlarge (4 vCPU) | Single-node may suffice for < 1 GB |
| 10-50 GB | 4-8 | xlarge | |
| 50-200 GB | 8-16 | 2xlarge (8 vCPU) | |
| 200 GB - 1 TB | 16-32 | 2xlarge | Consider 4xlarge if memory-bound |
| 1-5 TB | 32-64 | 4xlarge (16 vCPU) | |
| 5-20 TB | 64-128 | 4xlarge | Storage-optimized recommended |
| > 20 TB | 128+ | 4xlarge or 8xlarge | Consider splitting into stages |
| Concurrent Queries | Warehouse Size | Scaling |
|---|---|---|
| 1-5 | Small (2x workers) | Auto-stop after 10 min |
| 5-15 | Medium (4x workers) | Min 1, Max 3 clusters |
| 15-50 | Large (8x workers) | Min 2, Max 5 clusters |
| 50+ | X-Large (16x) | Min 3, Max 10+ clusters |
| Throughput | Workers | Instance | State Size |
|---|---|---|---|
| < 10 MB/s | 2-4 | xlarge | < 10 GB: general purpose |
| 10-100 MB/s | 4-8 | 2xlarge | 10-100 GB: memory-optimized |
| 100 MB/s - 1 GB/s | 8-16 | 4xlarge | > 100 GB: storage-optimized |
| > 1 GB/s | 16+ | 4xlarge+ | Consider multiple streams |
Available memory per executor = (Instance Memory - OS Overhead) × spark.memory.fraction
Where:
OS Overhead ≈ 10% of instance memory (min 384 MB)
spark.memory.fraction default = 0.6
| Workload Type | Memory per Core | Rationale |
|---|---|---|
| CPU-bound transforms | 4 GB / core | Minimal data in memory |
| Shuffle-heavy joins | 6-8 GB / core | Shuffle buffers and sort space |
| Broadcast joins | 8-12 GB / core | Broadcast table replicated per executor |
| Caching / ML | 12-16 GB / core | Large datasets in memory |
| Streaming with state | 16+ GB / core | State must fit in memory |
| Scenario | Autoscaling? | Min/Max Ratio |
|---|---|---|
| Scheduled batch job (predictable) | No — fixed size | N/A |
| Interactive notebook exploration | Yes | 1:4 |
| Variable pipeline (fan-out/fan-in) | Yes | 1:3 |
| Streaming (steady state) | No — fixed size | N/A |
| Shared cluster (multiple users) | Yes | 2:8 |
Min workers = steady-state need (floor)
Max workers = peak burst need (ceiling)
Scale-down interval: 60-120 seconds (avoid thrashing)
| Instance | vCPU | Memory | Best For |
|---|---|---|---|
| m5d.xlarge | 4 | 16 GB | General ETL |
| m5d.2xlarge | 8 | 32 GB | General ETL |
| r5d.xlarge | 4 | 32 GB | Joins, caching |
| r5d.2xlarge | 8 | 64 GB | Large joins |
| i3.xlarge | 4 | 30.5 GB | Shuffle-heavy, local SSD |
| i3.2xlarge | 8 | 61 GB | Large shuffle + spill |
| c5.2xlarge | 8 | 16 GB | CPU-bound transforms |
| Instance | vCPU | Memory | Best For |
|---|---|---|---|
| Standard_DS3_v2 | 4 | 14 GB | General ETL |
| Standard_DS4_v2 | 8 | 28 GB | General ETL |
| Standard_E4ds_v4 | 4 | 32 GB | Joins, caching |
| Standard_E8ds_v4 | 8 | 64 GB | Large joins |
| Standard_L4s | 4 | 32 GB | Shuffle-heavy |
| Standard_L8s_v2 | 8 | 64 GB | Large shuffle + spill |
| Instance | vCPU | Memory | Best For |
|---|---|---|---|
| n2-standard-4 | 4 | 16 GB | General ETL |
| n2-standard-8 | 8 | 32 GB | General ETL |
| n2-highmem-4 | 4 | 32 GB | Joins, caching |
| n2-highmem-8 | 8 | 64 GB | Large joins |
| n2-highcpu-8 | 8 | 8 GB | CPU-bound transforms |
1. Use spot/preemptible instances for workers — 60-90% savings on worker compute. Keep the driver on-demand.
2. Auto-terminate idle clusters — Set auto-stop to 10-30 minutes for interactive clusters.
3. Right-size the driver — Driver rarely needs to be larger than a worker. Use the same or one size smaller.
4. Use Photon for scan-heavy workloads — Photon has higher DBU rates but faster execution, often net cheaper.
5. Schedule jobs during off-peak — Some cloud providers offer lower spot prices at night.
6. Use the cost_per_query_estimator — Quantify before committing to a cluster change.
*Cluster Sizing Guide — Datanest Digital Spark Optimization Playbook*
*https://datanest.dev*
Get the full Spark Optimization Playbook and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.