How do you find staging rows absent from a target table when the match key spans two columns?
answer
- One predicate per key column
- Both equalities belong to the join condition
- Which target column proves a real match?
- The least portable spelling involves row values
- Ask whether either key column is nullable
basics
~20 sUse NOT EXISTS with one correlated equality per key column, ANDed together. It is the portable multi-column anti-join and extends to any number of columns. A LEFT JOIN with both equalities in ON plus an IS NULL test on a target key column works too.
solid answer
~50 sWrite the anti-join as `NOT EXISTS` and AND one correlated predicate per key column: ```sql SELECT s.order_id, s.line_no FROM staging_lines s WHERE NOT EXISTS ( SELECT 1 FROM order_lines t WHERE t.order_id = s.order_id AND t.line_no = s.line_no ); ``` This is the form that scales: a third key column is one more ANDed predicate, nothing else changes. The `LEFT JOIN` equivalent puts **both** equalities in `ON` and tests one of the target's key columns for `IS NULL`. Splitting the second condition into `WHERE` would turn it into an inner join and return nothing. Avoid the row-constructor form `(s.order_id, s.line_no) NOT IN (SELECT order_id, line_no FROM order_lines)`: engines differ on whether they accept it, and it inherits the NULL fragility of `NOT IN`. If either key column is nullable, plain equality never matches those rows, so they will always look "missing" until you compare them NULL-safely.
code
sql · 9 lines-- Portable composite-key anti-join
SELECT s.order_id, s.line_no, s.qty
FROM staging_lines s
WHERE NOT EXISTS (
SELECT 1
FROM order_lines t
WHERE t.order_id = s.order_id
AND t.line_no = s.line_no
);go deeper
Be able to extend the single-column NOT EXISTS pattern by ANDing a second correlated equality inside the subquery. Knowing that both columns must be compared is the core of it.
Explain why the LEFT JOIN version needs both equalities in ON and an IS NULL test on a target key column, and why matching on only part of a composite key over-excludes rows.
Show reconciliation judgment: run both directions, decide up front what NULLs in the key mean, and reject spellings that can fail silently on real data rather than raising an error.
Own the reconciliation contract itself — what the key is, whether it is enforced on the staging side, how discrepancies are reported and escalated. Argue for keys declared NOT NULL so the comparison question never arises.
## The task Reconciliation questions — which staged rows have not been loaded, which line items are missing from the archive, which of yesterday's records never arrived — are anti-joins on a **composite key**. The single-column patterns still apply, but the three spellings stop being equally practical, and the interviewer is checking whether you know which one degrades. ## NOT EXISTS: the form that scales ```sql SELECT s.order_id, s.line_no, s.qty FROM staging_lines s WHERE NOT EXISTS ( SELECT 1 FROM order_lines t WHERE t.order_id = s.order_id AND t.line_no = s.line_no ); ``` Everything about the single-column version carries over unchanged. The subquery is correlated on two columns instead of one; `EXISTS` still yields TRUE or FALSE with no third state; the result keeps one row per unmatched staging row, and every staging column stays available for projection so you can output the rows themselves rather than just their keys. Adding a third key column means adding a third ANDed line. No other form is this stable under change. ## LEFT JOIN with IS NULL: correct if you are careful ```sql SELECT s.order_id, s.line_no, s.qty FROM staging_lines s LEFT JOIN order_lines t ON t.order_id = s.order_id AND t.line_no = s.line_no WHERE t.order_id IS NULL; ``` Both equalities are part of the join condition. Only after the join do you test for NULL-extension, and you test a **key column of the target**, because a matched row cannot have a NULL there. Two mistakes are common in the composite case: moving the second equality to `WHERE`, which discards NULL-extended rows and silently returns nothing; and testing a nullable payload column of the target, which reports genuinely present rows as missing. ## NOT IN with a row-value constructor: portable in theory Standard SQL allows comparing a row value against a subquery's rows: ```sql SELECT s.order_id, s.line_no FROM staging_lines s WHERE (s.order_id, s.line_no) NOT IN ( SELECT t.order_id, t.line_no FROM order_lines t ); ``` In practice, engines differ on whether row-value constructors are accepted in `IN`, so this is the least portable spelling — check your engine's documentation before relying on it. It also inherits `NOT IN`'s NULL behaviour: a NULL in either projected column of the subquery can make the predicate never TRUE, so the query returns nothing at all. The failure is silent, which is the worst property a reconciliation query can have. ## Nullable key columns Composite keys in staging data are often not declared `NOT NULL`. Equality comparison never matches NULL to NULL, so any staging row whose key contains a NULL will be reported as missing even when a byte-identical row exists in the target. Decide first what a NULL means in your key: if it is genuinely "no value" and should match another absence, the comparison must be NULL-safe rather than a plain `=`; if it is "unknown", then "missing" is arguably the right answer and you should say so explicitly. Either way, state the decision — a reconciliation report that quietly treats unknown as missing produces phantom discrepancies every run. ## Practical framing A good answer sequences it: (1) name the pattern — this is an anti-join on a composite key; (2) give `NOT EXISTS` as the default and show that it grows by one predicate per column; (3) offer the `LEFT JOIN` form with its two conditions, both equalities in `ON` and `IS NULL` on a target key column; (4) flag the row-constructor `NOT IN` as portability- and NULL-sensitive; (5) raise nullable key columns as a question about the data before writing anything. Mentioning that you would run the mirror query — target rows missing from staging — completes the reconciliation, since the two directions answer different questions.
- How would you also report target rows that are missing from staging?Run the mirror query with the sides swapped: NOT EXISTS over staging_lines correlated from order_lines. The two directions answer different questions — rows not yet loaded versus rows in the target with no source — and a reconciliation report normally needs both, usually labelled so the reader can tell which direction each row came from.
- What if one of the two key columns is nullable?Plain equality never matches NULL to NULL, so those staging rows are reported as missing even when an identical target row exists. Decide what NULL means in the key first: if it should match another absence, compare NULL-safely; if it means unknown, then flagging the row may be correct, but say so rather than letting the report imply real data loss.
- Why prefer NOT EXISTS over the row-constructor NOT IN form here?NOT EXISTS is accepted everywhere, extends to a third or fourth key column by adding one ANDed predicate, and cannot be emptied by a NULL in the compared columns. Row-value constructors inside IN are not uniformly supported, and NOT IN over nullable columns can silently return no rows — the worst possible failure for a reconciliation query.
saying these in an interview costs you the question
- Matches on one key column and calls the rest close enough
- Puts the second equality in WHERE and still expects an anti-join
- Assumes every engine accepts row-value constructors in IN
- Ignores nullable key columns in reconciliation queries
- Tests IS NULL on a nullable payload column of the target