Why does a FULL OUTER JOIN result need COALESCE(l.customer_id, r.customer_id) for its key column?
answer
- the key survives as two separate columns
- each copy is NULL for one bucket
- pick whichever side is populated
- watch what GROUP BY does with NULLs
- alias the merged expression
basics
~20 sA full outer join NULL-extends whichever side is unmatched, so each key column is NULL for rows that came only from the other table. COALESCE(l.customer_id, r.customer_id) merges the two into one column that is always populated.
solid answer
~50 sAfter a `FULL OUTER JOIN`, the join keys survive as **two separate columns**. For a row that exists only on the left, `r.customer_id` is NULL; for a row that exists only on the right, `l.customer_id` is NULL. Selecting either column alone silently loses the identity of half your rows, so the idiom is `COALESCE(l.customer_id, r.customer_id) AS customer_id` — take the left value, fall back to the right. This matters most for `GROUP BY` and `ORDER BY`: grouping by `l.customer_id` lumps every right-only row into a single NULL group, which quietly corrupts totals. The same reasoning applies to any column present on both sides. One caveat: `COALESCE` cannot distinguish a manufactured NULL from a NULL that the source row actually stored, so keep a separate `l.customer_id IS NULL` test when you need to know which side a row came from.
code
sql · 12 lines-- Wrong: right-only customers all collapse into one NULL group
SELECT l.customer_id, SUM(COALESCE(r.amount, 0)) AS amount_2024
FROM orders_2023 l
FULL OUTER JOIN orders_2024 r ON l.customer_id = r.customer_id
GROUP BY l.customer_id;
-- Right: one always-populated key, grouped by the same expression
SELECT COALESCE(l.customer_id, r.customer_id) AS customer_id,
SUM(COALESCE(r.amount, 0)) AS amount_2024
FROM orders_2023 l
FULL OUTER JOIN orders_2024 r ON l.customer_id = r.customer_id
GROUP BY COALESCE(l.customer_id, r.customer_id);go deeper
Remember that the join key comes back twice, once per table, and that each copy is NULL for the rows that exist only on the other side. Reach for COALESCE to get one usable key column.
Explain the mechanics: which bucket makes which column NULL, why GROUP BY on a single side's key silently merges rows into one NULL group, and why the coalesced expression must be repeated in GROUP BY for portability.
Show that you separate identity from provenance — coalesce the key for output, but test the raw key columns to classify rows, and coalesce measures to a neutral value so downstream arithmetic does not go NULL.
Own the convention: agree how reconciliation outputs name their key and status columns so consumers do not each reinvent the NULL handling, and decide where that normalisation belongs in the pipeline.
## Two key columns, not one It is tempting to think of a join as merging rows on a shared key, leaving one key column behind. That is not what a join on `ON l.customer_id = r.customer_id` does. The output row has the left table's columns followed by the right table's columns, and the key appears **twice**. With an inner join that duplication is harmless: the predicate guaranteed the two copies are equal, so either one will do. With a `FULL OUTER JOIN` it stops being harmless, because either copy may be NULL: | bucket | l.customer_id | r.customer_id | |---|---|---| | matched | value | same value | | left-only | value | NULL | | right-only | NULL | value | Selecting `l.customer_id` alone reports NULL for every right-only row; selecting `r.customer_id` alone reports NULL for every left-only row. Neither column can serve as the result's identifier. ## The COALESCE idiom `COALESCE(a, b)` returns the first of its arguments that is not NULL, so: ```sql SELECT COALESCE(l.customer_id, r.customer_id) AS customer_id, l.orders_2023, r.orders_2024 FROM orders_2023 l FULL OUTER JOIN orders_2024 r ON l.customer_id = r.customer_id; ``` gives one always-populated `customer_id`. For matched rows both arguments are equal, so the choice does not matter; for left-only rows it yields the left value; for right-only rows the left argument is NULL and the right value is returned. The alias matters as much as the expression: without `AS customer_id` the output column has an implementation-defined name that downstream code cannot rely on. A closely related form is `JOIN ... USING (customer_id)`, which produces a single merged key column instead of two; that clause has its own semantics and restrictions, and the explicit `COALESCE` is the form you write when the key columns have different names or when you also want the raw per-side columns. ## Why grouping and ordering are the sharp edge Selecting the wrong key column produces a visibly odd result — a column of NULLs. Grouping by it produces a *plausible* but wrong result, which is far more dangerous: ```sql -- WRONG: every right-only customer collapses into one NULL group SELECT l.customer_id, SUM(COALESCE(r.amount, 0)) FROM a l FULL OUTER JOIN b r ON l.customer_id = r.customer_id GROUP BY l.customer_id; -- RIGHT SELECT COALESCE(l.customer_id, r.customer_id) AS customer_id, SUM(COALESCE(r.amount, 0)) FROM a l FULL OUTER JOIN b r ON l.customer_id = r.customer_id GROUP BY COALESCE(l.customer_id, r.customer_id); ``` Grouping treats all NULLs as one group, so in the wrong version every customer who appears only in the second table is merged into a single row keyed by NULL, and their measures are summed together. Ordering has a milder version of the same defect: all the right-only rows sort together at whichever end the engine places NULLs, rather than interleaving by key. Note that the grouping expression is repeated in the `GROUP BY`. Whether you may instead write `GROUP BY customer_id` referring to the select-list alias depends on the engine, so repeating the expression is the portable form. ## Beyond the key The same reasoning extends to any column that both sides carry. If both tables have `region` and you want the region regardless of which side supplied it, `COALESCE(l.region, r.region)` is the answer. For measures the pattern is usually different — `COALESCE(l.amount, 0)` turns a missing side into a zero so that arithmetic like `COALESCE(r.amount, 0) - COALESCE(l.amount, 0)` produces a number instead of NULL, which is what a delta column needs. ## The caveat: manufactured NULLs versus stored NULLs `COALESCE` treats every NULL the same, but a NULL in the output has two possible origins: the join manufactured it because there was no partner row, or the source row genuinely stored NULL there. For the join key under an equality predicate the ambiguity is limited — a stored NULL key never matches anything, so such a row lands in the unmatched bucket anyway — but for ordinary columns it is real. When your query must report *which side* a row came from, do not infer it from a coalesced value; test the key columns explicitly, for example `CASE WHEN l.customer_id IS NULL THEN 'right only' WHEN r.customer_id IS NULL THEN 'left only' ELSE 'both' END`, and prefer a column the source guarantees is not nullable for that test. ## Summary A full outer join hands you two key columns, each NULL for one third of the result. `COALESCE(l.key, r.key)` rebuilds the single identifier you actually wanted; use it in the select list, the `GROUP BY` and the `ORDER BY`, alias it, and keep the raw columns around when you still need to know which side a row came from.
- Would COALESCE(l.customer_id, r.customer_id) ever pick a value that disagrees with the other side?Not under an equality predicate: matched rows have equal keys by construction, so both arguments carry the same value and only one is ever needed. Under a non-equi `ON` condition — a range or inequality match — the two columns can legitimately differ, and coalescing them silently reports only the left one.
- How do you label which side each row came from after a FULL OUTER JOIN?Test the key columns directly: `CASE WHEN l.id IS NULL THEN 'right only' WHEN r.id IS NULL THEN 'left only' ELSE 'both' END`. Do not infer it from a coalesced column, which has erased the distinction. Choose a column the source table guarantees is populated, typically the primary key.
- What do you do with the measure columns, as opposed to the key?Usually `COALESCE(x, 0)` — or whatever the neutral value is — so that arithmetic still yields a number. `COALESCE(r.amount, 0) - COALESCE(l.amount, 0)` gives a usable delta, whereas `r.amount - l.amount` is NULL for every row that exists on only one side.
saying these in an interview costs you the question
- Selects only the left key and never notices the NULL rows
- Thinks the join merges the two key columns into one automatically
- Groups by one side's key and trusts the totals
- Believes COALESCE can tell a stored NULL from a join-made one
- Says NOT NULL on the source column makes the check unnecessary