Tune Partition Counts
Learn to tune partition counts for better performance in Databricks. This tutorial covers why partitions matter, how to choose the right count, hands-on examples, troubleshooting, and what to study next.
Focus: tune partition counts for better performance
Your Spark job runs, but it feels sluggish. Maybe it takes 15 minutes when it should take 5. You check the Spark UI and see hundreds of tiny tasks finishing in milliseconds — or worse, a few massive tasks that take minutes each, leaving most of your cluster idle. The culprit? Partition counts. In Databricks, the number of partitions you use for your DataFrames and RDDs can make or break your job's performance. Get it wrong, and you're paying for a 16-node cluster while using 2 nodes effectively. Get it right, and your jobs fly. This lesson shows you exactly how to tune partition counts for better performance — the what, the why, and the practical how — so you can stop guessing and start optimizing.
The problem this lesson solves
Imagine you've built an ETL pipeline that reads millions of rows, joins them, aggregates them, and writes them to Delta Lake. It works — but it's slow. You're wondering: Is this normal? Can I do better? The answer is almost always yes, and the most common performance killer is a poorly chosen partition count.
When Spark runs a job, it splits your data into partitions — chunks of data that different executors process in parallel. If you have too few partitions, you under-utilize your cluster: some executors sit idle while a handful of tasks grind through huge data blocks. If you have too many partitions, you drown the scheduler in tiny tasks, and the overhead of task creation, shuffling metadata, and serialization dominates. Both scenarios waste time and money on your Databricks cluster.
The pain is real. You might see:
- A join that causes a massive shuffle because keys aren't distributed well.
- A write to Delta Lake that creates thousands of tiny files, hurting future reads.
- A job that runs fine on a small dataset but collapses when you scale up.
This lesson equips you to diagnose and fix these issues by controlling partition counts proactively.
Core concept / mental model
Think of partitions as work assignments for your Spark cluster. Each executor is a worker, and each partition is a task that a worker picks up, processes, and reports back on. The goal is to assign enough tasks to keep everyone busy, but not so many that the coordination overhead slows everyone down.
A useful analogy: You're organizing a team to move a mountain of sand. If you give one person a shovel and tell them to move the whole mountain, it takes forever. If you give every person a teaspoon and break the sand into a million tiny piles, they spend more time walking between piles than scooping. The sweet spot is giving each person a shovel and a reasonable pile.
In Spark terms, the "shovel" is your cluster's total memory and CPU. The "pile size" is your partition size — typically a few hundred megabytes per partition.
Key definitions:
- Partition — a unit of data that Spark processes in a single task. It can be a block of a DataFrame or an RDD.
- Task — the smallest unit of work Spark schedules on an executor. One task processes one partition.
- Shuffle — the process of redistributing data across partitions during operations like
join,groupBy, ordistinct. Shuffles are expensive because they move data over the network. - Spark default partition count — For a DataFrame operation like
repartition, Spark uses a default of 200 partitions (controlled byspark.sql.shuffle.partitions). For RDD operations,spark.default.parallelismis used.
The mental model to internalize: partition count determines parallelism. More partitions mean more parallelism, but also more overhead. Your job is to find the balance where tasks are neither too small nor too large.
When you set partition counts, you're making a tradeoff:
- Too few partitions: Low parallelism → some cores idle, tasks take long, memory pressure on a single executor.
- Too many partitions: High parallelism → scheduling overhead, too many small tasks, shuffle write amplification, possible broadcast timeout.
How it works step by step
Tuning partition counts is not a black art. Follow this logical sequence:
1. Know your cluster size
First, determine how many executor cores you have. In Databricks, this depends on your cluster configuration (instance type and number of workers). The maximum useful parallelism is roughly 2–3 × total cores. So if you have 8 workers with 4 cores each (32 cores), aim for 64–96 partitions for CPU-bound work.
2. Estimate the target partition size
A good target is 100–300 MB per partition for most jobs. Why? Executors typically have a few GB of memory, and partitions in this range balance decompression overhead and task count.
Use this guideline:
target_partitions_ids = total_size_in_bytes / 200_MB
So for 10 GB of data, you'd want around 50 partitions (if 200 MB each). Adjust based on your cluster and operation type.
3. Apply the right partition operation
Spark gives you three common ways to adjust partitions:
repartition(n)— Full shuffle into exactlynpartitions. Use when you want to increase partitions or create a specific distribution. Expensive because it shuffles all data.coalesce(n)— Reduce partitions without a full shuffle, by merging existing partitions. Use when you have too many partitions and want to decrease the count. Cheaper but can cause data skew if partitions are uneven.repartitionByRange(col)— Range-based partitioning that samples data for balanced sizes. Often yields more even partitions thanrepartitionwith a column.
4. Configure Spark session defaults
You can set defaults in your Databricks notebook:
spark.conf.set("spark.sql.shuffle.partitions", 100)
spark.conf.set("spark.default.parallelism", 32)
The spark.default.parallelism setting affects RDDs and some DataFrame operations. The shuffle.partitions setting controls the number of partitions after a shuffle (e.g., after groupBy or join).
5. Test and iterate
Run your job, observe the Spark UI, and adjust. Look at:
- Task duration — Are tasks too short (milliseconds) or too long (minutes)?
- Shuffle sizes — Is the shuffle spilling to disk?
- Executor load — Are all executors processing data, or are some idle?
Pro tip: Use the Spark UI in Databricks. Navigate to the SQL or Jobs tab, find your stage, and click on Details. You'll see the distribution of task durations and shuffle sizes. That's your ground truth.
Hands-on walkthrough
Let's put this into practice with a simple DataFrame transformation. You'll run this in a Databricks notebook.
Step 1: Set up your environment
# Create a Spark session if not already present
spark = SparkSession.builder.appName("PartitionTuning").getOrCreate()
# Set the shuffle partition count for the session
spark.conf.set("spark.sql.shuffle.partitions", 100)
print("Shuffle partitions:", spark.conf.get("spark.sql.shuffle.partitions"))
Expected output: Shuffle partitions: 100
Step 2: Create a large dataset
from pyspark.sql import Row
import random
data = [(random.randint(1, 10000), "value" + str(i)) for i in range(10_000_000)]
df = spark.createDataFrame(data, ["id", "value"])
print("Initial partitions:", df.rdd.getNumPartitions())
Expected output: Initial partitions: 8 (or similar, based on your default parallelism)
You'll likely see only a few partitions initially, which is fine for a small in-memory list.
Step 3: Increase partitions to speed up a group-by
# Repartition into a larger count
repartitioned_df = df.repartition(100)
print("After repartition:", repartitioned_df.rdd.getNumPartitions())
# Do a group-by that triggers a shuffle
agg_df = repartitioned_df.groupBy("id").count()
agg_df.collect() # Action to trigger the job
Expected output: After repartition: 100 and the job runs. Observe the Spark UI to see task durations.
Step 4: Reduce partitions with coalesce before writing
# Coalesce down to 16 partitions before writing (to avoid small files)
coalesced_df = agg_df.coalesce(16)
print("After coalesce:", coalesced_df.rdd.getNumPartitions())
# Write to Delta
coalesced_df.write.format("delta").mode("overwrite").save("/tmp/partition-tuned-output")
Expected output: After coalesce: 16 and a Delta table with 16 files — much better than 100 tiny files.
Pro tip: Writing with too many partitions creates thousands of tiny files, which hurts Delta time travel and bloom filters. Aim for around 128–256 MB per file.
Compare options / when to choose what
| Operation | When to Use | Cost | Example Scenario |
|---|---|---|---|
repartition(n) |
Increase partitions for higher parallelism; redistribute data evenly | Full shuffle — expensive | You have 10 partitions but 32 cores; you want more parallelism for groupBy or join. |
coalesce(n) |
Decrease partitions when you have too many (e.g., before writing) | No shuffle, just merges existing partitions | You ended up with 1000 partitions after a filter; you want 50 before saving to Delta. |
repartitionByRange(col) |
Balance partitions by ranges of a key column | Shuffle with sampling | You have a skewed key in a join; range partitioning distributes keys more evenly. |
| Do nothing | Your partition count already matches cluster size | None | Your job runs in optimal time (60 partitions on 32-core cluster). |
Choosing the right count: A layperson's rule of thumb: partitions = total_cores * 2. So for 32 cores, 64 partitions is a good start. But always benchmark with your data size and operation type.
Why not always use more partitions? More partitions mean smaller tasks, but each task has overhead (e.g., scheduling, JVM garbage collection). Below 50–100 MB per partition, overhead often outweighs benefits.
Skewed data is a separate challenge. If one key dominates your data, repartition(n) won't help — you need salting (adding a random prefix to keys) or repartitionByRange. That's beyond this lesson, but you now know the basics.
Troubleshooting & edge cases
Here are concrete errors and wrong outputs you might encounter, along with fixes.
1. "Cannot coalesce to fewer partitions"
Cannot reduce the number of partitions to 1. This is usually caused by coalescing in a streaming query.
Cause: You tried coalesce(1) in a streaming context or right after an action that already transformed the data.
Fix: For streaming, use repartition instead. For batch, ensure you coalesce before any action that triggers a shuffle.
2. Shuffle spill to disk (Spark UI shows "Shuffle Spill (Disk)")
Cause: Your partitions are too large, or your executors have insufficient memory for the shuffle buffer.
Fix: Increase partition count to reduce partition size, or boost executor memory in cluster config. Also try spark.reducer.maxSizeInFlight tuning.
3. Job runs but tasks are under 100ms
Cause: Too many partitions for the data size. Task overhead dominates.
Fix: Reduce partitions — maybe halve the count and re-run. You'll see faster wall-clock time.
4. Skewed task durations: one task takes 40 minutes while others take 5 seconds
Cause: Data skew, often from a join key. Partitions are uneven.
Fix: Use repartitionByRange on the join key or implement salting (prefix keys with a random number within n salt ranges).
5. Out of Memory (OOM) on a single executor
Cause: A partition is too large for the executor's heap, or you have a broadcast join that tries to hold a huge table.
Fix: Increase partition count to shrink partition size, or adjust spark.sql.autoBroadcastJoinThreshold to avoid broadcasting big tables.
Key debugging steps:
- Open the Spark UI (Databricks auto-generates a link after each run).
- Click on the SQL tab to see query execution plans.
- Click into each stage and inspect duration, shuffle read/write, and spill.
- Adjust partition count and re-run — iterate.
Pro tip: Use
df.printSchema()anddf.explain()to understand how Spark plans your operations. If you see aExchange(shuffle) step, that's where partition count matters.
What you learned & what's next
You now understand how to tune partition counts for better performance in Databricks. You learned:
- The pain of bad partition counts — idle executors or scheduler overhead waste time and money.
- The mental model of partitions as work assignments, and how the shuffle works.
- A step-by-step approach to choose the right count based on cluster cores and data size.
- How to use
repartition,coalesce, andrepartitionByRangein practice, with code examples. - How to troubleshoot common issues like spills, skew, and OOM errors.
This is a foundational skill for any data engineer. Next, you'll build on this by learning optimizing shuffle partitions with AQE (Adaptive Query Execution), which automatically adjusts partition counts at runtime — giving you the best of both worlds. You'll see how Databricks' Delta Engine uses AQE to make your job even faster without manual tuning.
Keep an eye on the Spark UI after every change — that's where the real feedback lives. Happy tuning!
Practice recap
Try this in your Databricks workspace: create a 1 GB DataFrame, run a groupBy with the default 200 shuffle partitions, and note the time. Then set spark.sql.shuffle.partitions to 64 and re-run — compare timings. Finally, write the result to Delta with and without coalesce(8), and inspect the number of files written. You'll see the difference partition tuning makes in both runtime and output file count.
Common mistakes
- Setting
spark.sql.shuffle.partitionsto 200 (the default) for every job, regardless of data size or cluster size. This often leads to too many tiny partitions for small datasets, causing unnecessary overhead. - Using
repartitionwhen you only need to reduce partitions.repartitiontriggers a full shuffle, whilecoalescedoesn't. Usecoalescefor decreasing partition counts to save time. - Ignoring skew: tuning partition count won't fix a skewed join. You must address the key distribution (e.g., salting) or use range partitioning.
- Writing to Delta Lake with default partition count (e.g., 200) and creating hundreds of tiny files, which hurts read performance later. Always consider coalescing before write.
Variations
- Use
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")to let Adaptive Query Execution (AQE) automatically merge small partitions at runtime. - For critical ETL pipelines, benchmark different partition counts (e.g., 1x, 2x, 4x cores) and compare wall-clock times — treat partition count as a tunable hyperparameter.
- If your data is skewed, try
repartitionByRangeon the join key before the join, or add a salt column to evenly distribute keys.
Real-world use cases
- Optimizing a daily ETL that joins 50 GB of sales data to a dimension table — tuning partition counts cut runtime from 40 to 12 minutes on a 16-core cluster.
- Improving a Delta Lake write for a user analytics pipeline by coalescing to 32 partitions before
append— reducing 1,000+ small files to 32, speeding up subsequent reads. - Tuning a Spark streaming job that processes aggregated events by setting the shuffle partitions to 8 to match the micro-batch volume, lowering latency and avoiding task overhead.
Key takeaways
- Partition count = parallelism; too few under-utilizes cores, too many adds scheduling overhead.
- Aim for 100–300 MB per partition for most ETL jobs; adjust based on cluster cores and memory.
- Use
repartitionto increase or evenly distribute partitions; usecoalesceto cheaply reduce them. - Set
spark.sql.shuffle.partitionsbased on cluster size and data volume, not the default 200. - Always check the Spark UI for task duration and shuffle spill to guide your next tuning step.
- Skew is a different problem — partition count tuning won't fix it; use salting or range partitioning.
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.