Hook: Interviewers love this one because it reveals whether you can keep both matched and unmatched rows without accidentally turning the query into an inner join.
Question: What is a FULL OUTER JOIN?
Answer: A FULL OUTER JOIN returns every row from both tables. Where the join condition matches, the two rows are combined into one result row; where there is no match, the missing side is filled with NULL values, which means 'no value'.
Interview-Ready Answer: I use a FULL OUTER JOIN when I want a complete picture from both sides, not just the overlap. It keeps matched rows together, but it also preserves left-only and right-only rows by padding the missing side with NULL. That makes it ideal for reconciliation, audits, and spotting missing data, and I always remember that filtering in WHERE can accidentally remove those unmatched rows.
A join combines rows from two tables using a condition in the ON clause. In a FULL OUTER JOIN, the database keeps the matched pairs and also keeps rows that do not find a partner on either side. The unmatched side is padded with NULL columns, so the row still appears even though one table had no match.
ON condition for each potential match.NULL values for the right table columns.NULL values for the left table columns.NULL to NULL, because NULL means 'unknown', not 'equal'.| Join | Matched rows | Unmatched left | Unmatched right | Typical use |
|---|---|---|---|---|
| INNER | Yes | No | No | Only overlap |
| LEFT | Yes | Yes | No | Keep all left |
| RIGHT | Yes | No | Yes | Keep all right |
| FULL | Yes | Yes | Yes | Keep everything |
The optimizer may implement a FULL OUTER JOIN with a hash join (build an in-memory lookup, then probe the other side) or a merge join (walk sorted inputs together). In rough terms, a hash-based plan is often close to O(n + m) for the scan work, while a sort/merge plan is closer to O(n log n + m log m) if sorting is needed. Memory use can jump quickly on wide tables or million-row inputs, so indexes on the join keys and narrow projections help. Also, not every database supports FULL OUTER JOIN directly: PostgreSQL and SQL Server do, while MySQL still does not, so you may need to emulate it with a LEFT JOIN plus an anti-join UNION ALL.
WHERE can break the outer join. If you write WHERE right_table.status = 'paid', then left-only rows vanish because NULL = 'paid' is not true.NULL keys do not match. If you need null-safe matching, you need a dialect-specific approach or explicit handling.COALESCE for the display key.Memory hook: think of two guest lists after a party: FULL OUTER JOIN keeps everyone from both lists, pairs up the people who match, and leaves blank name tags for the missing side.
Imagine a checkout service that reconciles orders with payments every night. The product team wants to know three things: paid orders, orders that never got paid, and payment records that do not belong to any order. A FULL OUTER JOIN is perfect here because it exposes all three groups in one result set.
What goes wrong if someone misunderstands it? A junior engineer swaps in an INNER JOIN because 'we only care about matches'. The dashboard suddenly looks healthy, but it is lying: unpaid orders disappear, orphaned payments disappear, and finance thinks the pipeline is clean. In practice, support tickets spike with symptoms like 'customer charged but order missing', reconciliation logs say '0 mismatches', and the nightly report undercounts exceptions by a large margin.
-- FULL OUTER JOIN demo: keep matched rows, left-only rows, and right-only rows.
-- This is written in standard, widely-supported SQL style.
-- The row with customer_id = 5 has no customer match.
-- The row with NULL customer_id stays unmatched because NULL does not equal NULL.
WITH customers AS (
SELECT *
FROM (VALUES
(1, 'Ava'),
(2, 'Ben'),
(3, 'Cara'),
(4, 'Drew')
) AS c(customer_id, customer_name)
),
orders AS (
SELECT *
FROM (VALUES
(10, 1, CAST(25.00 AS DECIMAL(10,2))),
(11, 1, CAST(40.00 AS DECIMAL(10,2))),
(12, 3, CAST(15.00 AS DECIMAL(10,2))),
(13, 5, CAST(99.00 AS DECIMAL(10,2))),
(14, CAST(NULL AS INT), CAST(5.00 AS DECIMAL(10,2)))
) AS o(order_id, customer_id, amount)
)
SELECT
COALESCE(c.customer_id, o.customer_id) AS customer_id,
c.customer_name,
o.order_id,
o.amount,
CASE
WHEN c.customer_id IS NULL THEN 'right_only'
WHEN o.order_id IS NULL THEN 'left_only'
ELSE 'matched'
END AS row_type
FROM customers AS c
FULL OUTER JOIN orders AS o
ON c.customer_id = o.customer_id
-- If you add a WHERE filter on the right table here, you may accidentally drop left-only rows.
ORDER BY COALESCE(c.customer_id, o.customer_id), o.order_id;Follow-up & Tricky Questions:
WHERE c.customer_id IS NULL OR o.order_id IS NULL. That keeps the 'orphans' from either side and removes the matched pairs.LEFT JOIN plus a reversed LEFT JOIN for the right-only rows, then combine them with UNION ALL and an anti-join condition so matched rows are not duplicated.ON or WHERE? Put match logic in ON; use WHERE only for final result filtering when you are sure you do not want to lose unmatched rows.NULL keys matched together? Not with normal equality. NULL means unknown, so NULL = NULL is not true in standard SQL.WHERE right_table.status = 'paid' removes rows where the right side is NULL, which kills the unmatched left rows.Common Mistakes:
INNER JOIN when you need all rows. Correction: Use FULL OUTER JOIN when you must keep matched and unmatched rows from both sides.WHERE. Correction: Put match conditions in ON, or you may delete the very unmatched rows you wanted to keep.NULL keys match each other. Correction: Standard SQL does not treat NULL as equal to NULL.Memory Hook: 'Two guest lists, one party' — keep every guest from both lists, pair the overlaps, and leave blanks where one side had no guest.
Cheat Sheet:
NULL on the missing side.WHERE can accidentally remove unmatched rows.NULL does not match NULL with normal equality.UNION ALL.Practice Tasks:
employees and badges to find people without badges and badges without people.WHERE into ON and observe the difference.