What's changed: Initial version
3.3Joins and subqueries
Covers when to use inner/outer/self joins, the performance and semantic differences between correlated and uncorrelated (non-correlated) subqueries, set operations (UNION/INTERSECT/EXCEPT), and how NULL and three-valued logic affect conditional expressions and join results, together with judgment calls for query correctness in practice.
Joins and subqueries can often express the same result in more than one way, and judging which formulation is correct and which is faster is core practical skill for a database specialist. The handling of NULL in particular is an area where beginners commonly go wrong, because conditional expressions evaluate under three-valued logic—true, false, or "unknown"—rather than just true/false. This section covers choosing a join type, subquery varieties, set operations, and NULL pitfalls.
3.3.1Choosing a join type
- If you want to see only customers who have orders, an inner join suffices, but if you want to see every customer regardless of whether they have orders—showing NULL when they do not—a left outer join is required. Choosing the wrong join type is a common practical bug: rows that genuinely exist end up silently missing from the result.
- If you attach a condition on the joined table's column in the
WHEREclause after an outer join, that condition can implicitly exclude NULLs and silently revert the result to what an inner join would give (e.g.,LEFT JOIN orders ON ... WHERE orders.status = 'shipped'excludes customers with no orders at all). In that case, the condition should instead be placed in theONclause, or theWHEREclause should also allow forIS NULL.
3.3.2Correlated and uncorrelated subqueries
- An uncorrelated (non-correlated) subquery does not reference the outer query's values and can be executed independently just once. Its result is a single value or a fixed list, making it easy to combine with
INor a comparison operator. - A correlated subquery references the outer query's per-row values, so the subquery is conceptually re-evaluated once for every outer row. It is typically paired with
EXISTS/NOT EXISTSto test "does a related row exist or not"; the doubleNOT EXISTSidiom for relational division discussed earlier is one instance of this pattern. - Although a correlated subquery is conceptually re-run once per outer row, many optimizers rewrite correlated subqueries into a join (a semi-join or anti-semi-join) for efficiency, so performance does not necessarily degrade linearly with a naive per-row execution. However, for sufficiently complex subqueries the optimizer may fail to rewrite them, causing genuinely repeated execution and real performance degradation—so checking the execution plan is essential.
3.3.3Set operations and the three-valued logic of NULL
- UNION combines two query results and removes duplicates (
UNION ALLkeeps duplicates). INTERSECT keeps only rows common to both. EXCEPT keeps rows present in one but not the other. All three require matching column count and compatible data types (union-compatibility). - SQL conditional expressions are evaluated under three-valued logic, which has UNKNOWN in addition to TRUE and FALSE. Any comparison involving NULL (e.g.,
= NULL) always evaluates to UNKNOWN, and aWHEREclause excludes UNKNOWN rows from the result (only TRUE rows are kept). - Testing for NULL requires IS NULL/IS NOT NULL, not
= NULL. A well-known trap is that if even a single NULL appears in the result list of aNOT INsubquery, the comparison becomes UNKNOWN and the entire outer result unexpectedly comes back empty (usingNOT EXISTSinstead avoids this problem).
Most-tested: "attaching a WHERE condition on the joined table's column after an outer join can silently revert it to an inner join," "if NULL appears in a NOT IN subquery's result list, the whole outer query returns empty (avoidable with NOT EXISTS)," and "any comparison with NULL evaluates to UNKNOWN, so IS NULL is required." Also remember that optimizers often rewrite correlated subqueries into joins.
Suppose an application developer writes a query for "all customers except those who canceled an order" as customer_id NOT IN (SELECT customer_id FROM cancellations), and then reports a bug: no customers show up at all. The first step in diagnosing this is to check whether the cancellations.customer_id column contains any NULL. If even one row in cancellations was inserted with customer_id left unset (NULL) for some systemic reason, NULL ends up in the NOT IN subquery's result list, and under three-valued logic, the comparison "customer_id NOT IN (..., NULL, ...)" evaluates to UNKNOWN for every row, so the WHERE clause keeps none of them. There are two possible fixes: (1) add WHERE customer_id IS NOT NULL to the subquery to exclude the NULL, or (2) rewrite NOT IN as NOT EXISTS (a correlated subquery)—NOT EXISTS (SELECT 1 FROM cancellations c WHERE c.customer_id = customers.customer_id) performs an individual equality check each time and is unaffected by a stray NULL. In practice, from a recurrence-prevention standpoint, rewriting to NOT EXISTS (option 2) should be the standard, with option 1 treated as a stopgap fix. That is because another developer could write a similarly structured NOT IN subquery in the future and fall into the same trap; establishing a team coding convention that "exclusion-condition subqueries should generally use NOT EXISTS" is the more fundamental countermeasure.
| Expression | Evaluation |
|---|---|
| NULL = NULL | UNKNOWN (never TRUE) |
| column = NULL | UNKNOWN |
| column IS NULL | TRUE or FALSE (correctly tests for NULL) |
| NULL present in a NOT IN list | The whole comparison becomes UNKNOWN, potentially yielding an empty result |
Trap: "NOT IN and NOT EXISTS always return the same result, so they can be freely swapped" is wrong—when the subquery's result contains NULL, NOT IN can unexpectedly return an empty result, while NOT EXISTS is unaffected, a significant difference. Also wrong: "a correlated subquery is always executed once per row and is therefore slow"—many optimizers rewrite it into a join for efficiency, so this cannot be assumed in general.
3.3.4Section summary
- Attaching a WHERE condition on the joined table's column after an outer join risks silently reverting it to an inner join; put the condition in ON or make WHERE NULL-tolerant
- A NOT IN subquery whose result contains NULL can yield an unexpectedly empty result; NOT EXISTS avoids this trap
- Comparisons with NULL evaluate to UNKNOWN under three-valued logic, so testing for NULL requires IS NULL, not
=
Sign in to track progress — Log in.
Quick check
(just a quick review)Q1. A query using `customer_id NOT IN (SELECT customer_id FROM cancellations)` returns zero rows, even for customers who never canceled. What is the most likely cause?
Q2. You want to list every customer regardless of whether they have orders, but writing `LEFT JOIN orders ON customers.id = orders.customer_id WHERE orders.status = 'shipped'` caused customers with no orders to vanish from the result. What is the appropriate fix?
Q3. Which statement most accurately describes the performance of a query using a correlated subquery?

