Turns out that is that this is essentially a flaw of Spark, and the correct solution is to use Iceberg to partition data to use storage-partitioned joins: https://medium.com/expedia-group-tech/turbocharge-efficiency-slash-costs-mastering-spark-iceberg-joins-with-storage-partitioned-join-03fdc1ff75c0.