FULL OUTER JOIN keeps every row from both tables — matched pairs, unmatched left rows (with NULLs on the right), and unmatched right rows (with NULLs on the left).
-- PostgreSQL, SQLite 3.39+, BigQuery, Snowflake, DuckDB
SELECT c.id AS customer_id, o.id AS order_id
FROM customers c
FULL OUTER JOIN orders o ON o.customer_id = c.id;
MySQL has no FULL OUTER JOIN, in any version. Emulate it: a LEFT JOIN covers matched rows and left-only rows; UNION ALL a RIGHT JOIN filtered down to just the right-only rows to add what the LEFT JOIN couldn’t see.
-- MySQL: LEFT JOIN (matched + left-only) UNION ALL RIGHT JOIN anti-joined to right-only
SELECT c.id AS customer_id, o.id AS order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
UNION ALL
SELECT c.id AS customer_id, o.id AS order_id
FROM customers c
RIGHT JOIN orders o ON o.customer_id = c.id
WHERE c.id IS NULL;
Dialect notes:
- PostgreSQL, BigQuery, Snowflake, DuckDB:
FULL OUTER JOIN(or justFULL JOIN— same thing). - SQLite:
FULL OUTER JOINandRIGHT JOINwere only added in 3.39.0 (2022). On an older SQLite, use the MySQL-style emulation below too. - MySQL: no
FULL OUTER JOINin any released version — always emulate.
Why
WHERE c.id IS NULLand not a plainUNIONof twoLEFT/RIGHTjoins. A simpler-looking emulation —LEFT JOIN ... UNION LEFT JOIN swapped— relies onUNION’s implicit de-duplication to collapse the matched rows that both halves produce, which forces a sort/hash pass over the whole result. Anti-joining the second half withWHERE c.id IS NULL(matched rows already came from theLEFT JOIN, so this is left-side-missing = right-only) means each row is produced exactly once, soUNION ALLis correct and cheaper thanUNION.