I'm trying to left join 2 tables on Spark 3, with 17M rows (events) and 400M rows (details). have an EMR cluster of 1 + 15 x 64core instances. (r6g.16xlarge tried with similar r5a) Source files are unpartitioned parquet loaded from S3.
this is the code I'm using to join:
join = (
broadcast(events).join(
details,
[
details["a"] == events["a2"],
(unix_timestamp(events["date"]) - unix_timestamp(details["date"])) / 3600
> 5,
],
"left",
)
).drop("a")
join.checkpoint()
To partition I'm using this:
executors = 15 * 64 * 3 # 15 instances, 64 cores, 3 workers per core
so I tried:
details = details.repartition(executors, "a")
and
details = details.withColumn("salt", (rand(seed=42) * nSaltBins).cast("int"))
details = details.repartition(executors, "salt")
In both scenarios, 90% of the workers end in around 5-10 minutes and the rest continue for a LONG time (50+minutes), long green line, no memory or disk errors on the log.
There is a little skewness after partitioning (all partitions between 180k and 160k rows), nothing accountable for more than 50 minutes of processor time.
Any idea of what I could be overseeing? Read a ton of posts and still feel that the green lines (worker time) should be closer between each other, they are all starting at the same time, they are not waiting for a worker to end.
Thanks!
--Edit--- Removed broadcast
On job 11, stage 17 it does 974/1000 in 2 minutes and 30 min later still on 993/1000, previous step uses the salted partitions (given by the executors variable) and it's very fast.
Execution plan:
Using 17906254 events
== Physical Plan ==
AdaptiveSparkPlan (13)
+- Project (12)
+- SortMergeJoin LeftOuter (11)
:- Sort (4)
: +- Exchange (3)
: +- Project (2)
: +- Scan parquet (1)
+- Sort (10)
+- Exchange (9)
+- Exchange (8)
+- Project (7)
+- Filter (6)
+- Scan parquet (5)
An example of 2h and more of 25% of that time is 1 executor remaining

Current spark configuration:
spark = SparkSession.builder.appName('Test').config("spark.driver.memory", "108g").config(
"spark.executor.instances", "59").config("spark.executor.memoryOverhead", "13312").config(
"spark.executor.memory", "108g").config("spark.executor.cores", "15").config("spark.driver.cores", "15").config(
"spark.default.parallelism", "1770").config("spark.sql.adaptive.enabled", "true").config(
"spark.sql.adaptive.skewJoin.enabled", "true").config("spark.sql.shuffle.partitions", "885").getOrCreate()
