20. Left, right, and full outer joins

📖 Reading · 4 min
💡 Every code box below is live — edit it and hit Run.

An outer join keeps rows that don't match, filling the missing side with nulls. Which side you preserve is the whole game.

Left join — keep every left row

how="left" keeps all rows of the left DataFrame; unmatched right columns become null. Our orphan order (customer c019, not in customers) now survives — with null customer fields:

orders.dropDuplicates().join(customers, "customer_id", "left").filter(
    F.col("name").isNull()
).select("order_id", "customer_id", "name").show()

That's the classic use of a left join: find the rows that didn't match by filtering for nulls on the right side. Order 1037 points at a customer who doesn't exist.

Right join — keep every right row

how="right" is the mirror image: every customers row survives, even those with no orders (c021, c022):

customers.join(orders.dropDuplicates(), "customer_id", "right").select(
    "customer_id", "name", "order_id"
).count()

a.join(b, key, "right") is the same as b.join(a, key, "left") — most people just always use left and swap the sides.

Full outer — keep everything

how="full" (or "outer") keeps all rows from both sides, matching where it can and nulling where it can't — orphan orders and order-less customers all appear:

orders.dropDuplicates().join(customers, "customer_id", "full").select(
    "order_id", "customer_id", "name"
).count()

Your turn

Left-join deduplicated orders to customers, then keep only the orders whose customer is unknown (customer name is null). Return order_id and customer_id. Assign to result.

result = orders  # <- dropDuplicates, left join customers, keep name IS NULL, select order_id & customer_id
result.show()
result = (
    orders.dropDuplicates()
    .join(customers, "customer_id", "left")
    .filter(F.col("name").isNull())
    .select("order_id", "customer_id")
)