A LEFT JOIN Can Lose Its Unmatched Rows When the Filter Moves

Two differently coloured stacks of index cards on a data analyst’s desk.
Conceptual illustration created with AI; not a research result or laboratory photograph.

You want every customer, together with any paid orders. A LEFT JOIN sounds appropriate, yet one customer disappears. The location of the paid-order filter may explain why.

Keep the example small

Imagine customers Ada and Ben. Ada has one paid order; Ben has none. In this query the payment condition helps decide which orders match:

SELECT c.name, o.id
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id
 AND o.status = 'paid';

Ada appears with an order identifier. Ben remains, with NULL in the order columns. If you instead put o.status = 'paid' in WHERE, Ben’s NULL-extended row does not pass that condition and is removed. PostgreSQL explains the distinction in its table-expression documentation: outer conditions apply after the join’s own matching step.

Check the business question before fixing the code

“All customers with any matching paid orders” and “only customers who have a paid order” are different requests. The second result may be exactly what you need. The bug lies in a mismatch between the request and the query, not in WHERE itself.

Add a test customer who has only an unpaid order as well as a customer with no orders. Confirm which names should remain in both cases. Finally, remember that several paid orders can still create several rows for one customer. Preserving unmatched customers does not make the output one row per customer; aggregation is a separate decision.

AI-generated editorial illustration; not a documentary photograph.