Bucketing splits a table into a fixed number of buckets when you write it. Rows with the same key go to the same bucket. Spark can then join two such tables without a shuffle.
Spark keeps the bucket layout in the catalog, which is the list of tables that Spark knows. So you need saveAsTable. A plain path has no place to keep the layout.
We use 3 buckets and the model from the joins chapter: bucket = customer_id % 3. Real Spark uses a hash, so real buckets differ. Here is the placement.
Rows in each bucket (our model)
| Bucket | orders in it | customers in it |
|---|
| 0 | 104, 105 (customer 3) | 3 Carla |
| 1 | 101, 102 (customer 1), 106 (customer 4) | 1 Asha, 4 Dev |
| 2 | 103 (customer 2) | 2 Ben, 5 Omar |
Here is the code that writes both tables in this layout.
Write both tables bucketed by customer_idworked example
(orders.write.bucketBy(3, "customer_id").sortBy("customer_id")
.saveAsTable("orders_bucketed"))
(customers.write.bucketBy(3, "customer_id").sortBy("customer_id")
.saveAsTable("customers_bucketed"))
bucketBy(3, ...) sets the bucket count. sortBy sorts the rows inside each bucket. Real tables use more buckets, such as 64.
"Joins in Spark" showed the plain plan: two Exchange lines, one for each table.
Now join the two bucketed tables. The first line turns off broadcast, because our tables are tiny.
Join the two bucketed tablesworked example
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)
o = spark.table("orders_bucketed")
c = spark.table("customers_bucketed")
o.join(c, "customer_id").explain()
spark.table reads the tables that saveAsTable created. explain() prints the plan and runs no job.
Here is the plan. It is simplified. Real output also has AdaptiveSparkPlan and Project lines, Filter isnotnull lines and column ids.
Plan for two bucketed tables (simplified)worked example
== Physical Plan (simplified) ==
SortMergeJoin [customer_id], [customer_id], Inner
:- Sort [customer_id ASC]
: +- FileScan parquet orders_bucketed (Bucketed: true)
+- Sort [customer_id ASC]
+- FileScan parquet customers_bucketed (Bucketed: true)
There is no Exchange line, so there is no shuffle. The task for bucket 1 reads orders 101, 102, 106 and customers 1, 4 from disk.
The join is still a SortMergeJoin. It only skips the move.
The Sort lines stay. In Spark 3, a bucketed scan does not tell Spark that the rows are sorted. Bucketing saves the shuffle, not the sort.
Bucketing also helps a groupBy on the same key, because equal keys already sit in one bucket. You pay once, when you write the table.
It helps for a big table that you join many times on the same key. For a join you run once, it is not worth the write.