Use salting when fixes 1 to 3 do not work. A salt is a random number in a new column, next to the key.
The key is now the pair (customer_id, salt). A key with 3 different salts looks like 3 different keys, so it goes to up to 3 partitions.
Salting has four steps. We use N = 3 salts, 0 to 2.
- Put a random salt, 0 to N - 1, in a new column on every row of the big side.
- Copy each row of the small side N times, one copy for each salt. This is called exploding.
- Join on (customer_id, salt) instead of customer_id.
- For an aggregate, add up twice: first by (customer_id, salt), then by customer_id. Step 2 puts Asha's 3 pieces back into one row.
We add up every order, in any status. Ben's 60.00 is a refunded order.
This is the code for the job "join each order to its customer, then find the total amount for each customer".
Salt the join and add up twiceworked example
N = 3
salted_orders = orders.withColumn("salt", (F.rand() * N).cast("int"))
salts = spark.range(N).select(F.col("id").cast("int").alias("salt"))
salted_customers = customers.crossJoin(salts) # 5 x 3 = 15 rows
joined = salted_orders.join(salted_customers, ["customer_id", "salt"])
step1 = joined.groupBy("customer_id", "salt").agg(F.sum("amount").alias("partial"))
step2 = step1.groupBy("customer_id").agg(F.sum("partial").alias("total"))
F.rand() is random, so a real run gives other salts. The next tables show one possible run.
Here is one possible run. The salts are made up so we can check the numbers.
We still use a model, not Spark's real hash: new partition = (customer_id + salt) % 3. The + only picks a partition. The key stays the pair, so (1, 1) and (2, 0) share partition 2 but never match each other.
| Key (customer_id, salt) | Orders in it | Rows | New partition |
|---|
| (1, 0) | 101, 201, 202, 203, 204 | 5 | new partition 1 |
| (1, 1) | 102, 205, 206, 207, 208 | 5 | new partition 2 |
| (1, 2) | 209, 210, 211, 212 | 4 | new partition 0 |
| (2, 0) | 103 | 1 | new partition 2 |
| (3, 1) | 104 | 1 | new partition 1 |
| (3, 2) | 105 | 1 | new partition 2 |
| (4, 0) | 106 | 1 | new partition 1 |
Count the rows in each new partition.
Partition 0 has 4 rows. Partition 1 has 5 + 1 + 1 = 7 rows. Partition 2 has 5 + 1 + 1 = 7 rows.
Before salting, the sizes were 2, 15 and 1. Now they are 4, 7 and 7. The largest task is less than half the size.
Every order still finds its customer. Order 201 has salt 0, so it looks for (1, 0). That key is in the exploded customers table.
Now check that the answer did not change. Step 1 adds up each (customer_id, salt) key.
Step 1 and step 2: the total is the sameworked example
step2.show()
show() prints (4 rows). Row order can change.
| customer_id | total |
|---|
| 1 | 185.00 |
| 2 | 60.00 |
| 3 | 15.50 |
| 4 | 80.00 |
Asha: 80.00 + 65.00 + 40.00 = 185.00. This is the same as 40.00 + 25.00 + 12 x 10.00 = 185.00 without salting. Carla: sum skips the NULL, so 15.50.
The partial sum for (1, 0) is 40.00 (order 101) + 4 x 10.00 = 80.00. The partial sum for (3, 2) is NULL, because order 105 has a NULL amount.
The totals match the totals without salting. Salting changes where rows go, not the answer.