Back to posts
Post

Solving Spark Small Files After Shuffle with Adaptive Coalesce

Enable spark.sql.adaptive.coalescePartitions.enabled to reduce small files post-shuffle and improve Spark job performance in practice.

Big DataApache Sparkperformance tuning

I’ve run into the small files problem in Spark more times than I can count — especially after shuffle stages where you end up with thousands of tiny Parquet files that kill downstream performance. It’s frustrating because the logic of your job is fine, but the physical layout hurts. In my environment, enabling adaptive query execution (AQE) and specifically turning on coalesce post-shuffle has made a repeatable difference. Here’s how I approach it.

Why small files happen after shuffle

When Spark performs a shuffle — say, during a groupBy or join — it writes intermediate data to disk partitioned by the shuffle keys. If the data is skewed or the initial partitioning is too fine, you get lots of small output files. These aren’t just annoying; they cause excessive task launches, metadata overhead, and poor columnar scan efficiency in formats like Parquet or ORC. I’ve seen jobs where 90% of the runtime was just task scheduling overhead from 50,000+ files under 10KB each.

How adaptive coalesce helps

Spark 3.0+ introduced adaptive query execution, which can replan stages based on runtime statistics. One part of this is spark.sql.adaptive.coalescePartitions.enabled. When true, after a shuffle stage, Spark evaluates whether the resulting partitions are too small and merges them into fewer, larger ones before proceeding. This doesn’t change the logic — it just reduces the number of write tasks and output files.

I set this alongside the core AQE flag:

spark.sql.adaptive.enabled=true
spark.sql.adaptive.coalescePartitions.enabled=true
spark.sql.adaptive.coalescePartitions.minPartitionNum=10
spark.sql.adaptive.advisoryPartitionSizeInBytes=128MB

The advisory size tells Spark what a good partition size looks like; the minPartitionNum prevents over-coalescing. In my ETL pipelines on YARN, this dropped post-shuffle file counts from ~20k to under 500 with no data loss and a 3–5x speedup in the write phase.

Verifying it’s working

You can check if coalesce happened in the Spark UI. Look for the shuffle read stage — if the number of output partitions is less than the number of input partitions to that stage, and you see a "Coalesce Partitions" node in the physical plan, it’s active. I also like to glance at the SQL tab for the explain plan:

EXPLAIN COST SELECT user_id, COUNT(*) FROM events GROUP BY user_id

If AQE is on, you’ll see a note about adaptive planning and possibly a coalesce step.

Caveats I’ve hit

This isn’t magic. If your data is extremely skewed, coalescing won’t fix the underlying imbalance — you might still get a few huge partitions and many small ones. In those cases, I combine this with salting or custom partitioning. Also, coalescing adds a small barrier shuffle step, so if your shuffle is already optimal, you might add latency. That’s why I tune the advisory size based on actual cluster I/O and file format.

As I mentioned before in my tuning notes, I always start with the defaults and adjust upward only if I see persistent small files after enabling AQE.

When to use it

I enable this by default on all production Spark jobs that involve shuffles — especially ETL, aggregations, and joins writing to data lakes. It’s low-risk, reversible, and the gains are consistent. If you’re using Spark 3.2+ and seeing small files after shuffle, turn it on and measure the output.

Final thoughts

The small files problem isn’t going away, but adaptive coalesce gives you a runtime lever to control it without rewriting jobs. Pair it with proper partitioning and monitoring, and you’ll spend less time fighting file counts and more time on actual data work.


Cover image: learn_tek · CC0 (Openverse / kamu malı) · https://www.flickr.com/photos/153724200@N07/27774351928