PostgreSQL Full Join

FULL JOIN returns every row from both tables, matching where possible and filling in NULLs on whichever side lacks a corresponding row.

How FULL JOIN Works

FULL JOIN, also called FULL OUTER JOIN, is the union of what LEFT JOIN and RIGHT JOIN would each produce. Matched rows appear once with data from both sides; left-only rows appear with NULLs on the right; right-only rows appear with NULLs on the left. No row from either table is ever dropped.

Basic Full Join

SELECT c.name AS customer, o.id AS order_id
FROM customers AS c
FULL JOIN orders AS o ON o.customer_id = c.id;

Finding All Unmatched Rows on Either Side

FULL JOIN is especially useful for reconciliation tasks, like comparing two datasets that should mostly overlap and surfacing records present in only one of them.

Reconciling Two Tables

SELECT COALESCE(a.sku, b.sku) AS sku,
       a.quantity AS warehouse_qty,
       b.quantity AS storefront_qty
FROM warehouse_stock AS a
FULL JOIN storefront_stock AS b ON a.sku = b.sku
WHERE a.sku IS NULL OR b.sku IS NULL;

Full Join vs Union of Left and Right

Conceptually, FULL JOIN a b ON condition produces the same rows as a LEFT JOIN of a to b combined with a RIGHT JOIN of a to b, deduplicated. PostgreSQL computes this directly and efficiently rather than literally running two joins.

Full Join with Aggregation

SELECT COALESCE(c.name, 'Unknown') AS customer,
       COUNT(o.id) AS order_count
FROM customers AS c
FULL JOIN orders AS o ON o.customer_id = c.id
GROUP BY c.name;
ScenarioLeft Row PresentRight Row Present
Matched pairYesYes
Left-only rowYesNULL
Right-only rowNULLYes
Note: FULL JOIN is used far less often than INNER or LEFT JOIN in everyday application queries, but it is indispensable for data quality checks and migrations where you need to see everything, matched or not.
Note: When you need to display a single identifying column that could come from either side, wrap it in COALESCE(left.col, right.col) so the result shows whichever value is actually present.