4.4.6 · D5Databases

Question bank — Joins — INNER, LEFT, RIGHT, FULL OUTER, CROSS, SELF

1,543 words7 min readBack to topic

Running tables (same as the parent note): Employees = {Asha·10, Bilal·20, Cara·NULL}, Departments = {10·Sales, 30·Legal}.


True or false — justify

A CROSS JOIN always produces more rows than the corresponding INNER JOIN.
Usually but not guaranteed. INNER is a subset of the Cartesian product, so it can never have more rows — but if the ON condition matches every pair, INNER equals CROSS (same count). If one table is empty, both are 0.
A LEFT JOIN B and B LEFT JOIN A return the same rows.
False. They preserve different tables. A LEFT JOIN B keeps all of A padding B with NULL; the reverse keeps all of B. They agree only when every row on both sides has a match (i.e. the join is effectively INNER).
FULL OUTER JOIN is just LEFT JOIN plus RIGHT JOIN concatenated.
Almost: it is the union (LEFT ∪ RIGHT), not a plain concatenation. The matched rows appear in both LEFT and RIGHT results, so a naive concat would double-count the INNER rows; UNION removes that overlap.
An INNER JOIN on dept_id will include Cara (dept_id = NULL).
False. NULL means "unknown", and NULL = NULL evaluates to unknown, not true — so Cara matches nothing and INNER drops her. Only outer joins can retain a NULL-keyed row.
A RIGHT JOIN can always be rewritten as a LEFT JOIN.
True. A RIGHT JOIN BB LEFT JOIN A — you just swap the table order. This is why many style guides ban RIGHT joins: every query can read left-to-right.
A SELF JOIN needs a special SQL keyword.
False. There is no SELF JOIN keyword. It is an ordinary join where the same table appears twice under two different aliases (Employees a JOIN Employees b).
Adding more join conditions to an INNER JOIN's ON clause can only shrink or keep the result size.
True. Each extra AND condition is an additional filter on the Cartesian product; a stricter predicate can never let more rows survive.
CROSS JOIN has no ON clause, so it cannot be filtered.
False. You can add a WHERE afterward — and a CROSS JOIN followed by a WHERE matching condition is identical to an INNER JOIN. The distinction is syntactic, not semantic.

Spot the error

SELECT ... FROM E LEFT JOIN D ON E.dept_id=D.dept_id WHERE D.dept_name='Sales' — still a LEFT JOIN?
No, it silently becomes INNER. Padded rows have D.dept_name = NULL, and NULL = 'Sales' is not true, so WHERE deletes every unmatched left row. Move the condition into ON to keep LEFT behaviour.
FROM Employees e, Departments d with no WHERE — what's the bug?
An accidental CROSS JOIN (Cartesian explosion). The old comma syntax lets you forget the join condition; you get rows instead of the matched few. Prefer explicit JOIN ... ON.
SELECT a.name, b.name FROM Employees a JOIN Employees b ON a.dept_id=b.dept_id — what's wrong?
==Missing a.emp_id < b.emp_id==. Without it each pair appears twice (A,B and B,A) and every employee pairs with themselves (A,A). The strict inequality yields each unordered distinct pair once.
... LEFT JOIN D ON E.dept_id=D.dept_id WHERE D.dept_id IS NULL to find matched employees — right?
Backwards. This finds unmatched employees (the padded NULLs), i.e. the anti-join. To find matched employees you'd test D.dept_id IS NOT NULL or use INNER JOIN.
SELECT COUNT(*) FROM E INNER JOIN D ON E.dept_id=D.dept_id used to count all employees — is it correct?
No. INNER drops employees with no matching department (Bilal, Cara), so the count is too low. To count all employees, COUNT(*) a LEFT JOIN from Employees, or just count Employees directly.
... ON e.dept_id = d.dept_id OR e.dept_id IS NULL to include NULL-dept employees — safe?
Risky. The OR ... IS NULL makes every NULL-dept row match every department row, inflating results. To simply retain Cara once, use a LEFT JOIN instead.

Why questions

Why does putting a right-table filter in ON preserve LEFT-JOIN rows but the same filter in WHERE destroys them?
ON filters before padding; WHERE filters after. ON decides which right rows are eligible to match; unmatched left rows still get NULL-padded and kept. WHERE runs on the finished result, and a NULL fails the equality test, so the padded row is removed.
Why can't NULL = NULL be used to match two unknown keys?
Because NULL means unknown value, and comparing two unknowns yields unknown (three-valued logic), which is not TRUE. Join conditions keep only rows where the predicate is TRUE, so NULLs never join. See NULL semantics in SQL.
Why do joins exist at all — why not store everything in one big table?
Because normalization splits data to avoid duplication and update anomalies; a department's name is stored once, not on every employee row. Joins reconnect that split-apart data at query time. See Normalization.
Why is a join conceptually a filtered Cartesian product even though the engine never builds all rows?
The relational-algebra meaning is — filter the product. The optimizer uses indexes and hash tables to compute the same answer efficiently, but the semantics are defined by the product-then-filter. See Relational Algebra.
Why does a foreign key make INNER and LEFT joins agree in one direction?
A foreign key guarantees every child value exists in the parent, so no child row is ever unmatched. Joining child→parent, INNER and LEFT return the same rows (nothing to pad). The reverse direction can still have orphan parents.
Why might INNER JOIN be faster than an equivalent LEFT JOIN in practice?
INNER can drop rows early and reorder the tables freely to use the smaller side or best index. LEFT must preserve every left row, constraining the optimizer's freedom and forcing NULL-padding work.
Why do indexes matter for join performance?
Without an index, matching rows means scanning the whole other table per row (nested loop, ). An index on the join key lets the engine look up matches directly, turning the search into roughly or a hash probe.

Edge cases

What does E LEFT JOIN D return when D is completely empty?
All employee rows, every right column NULL. LEFT preserves the left table regardless; with no possible matches, every row is padded. INNER, by contrast, would return zero rows.
What does INNER JOIN return when one table is empty?
Zero rows. The Cartesian product with an empty table is empty (), and filtering an empty set stays empty. Same for CROSS.
If two employees both have dept_id = 10, how many rows does INNER JOIN with Sales produce?
Two rows. Joins are many-to-one/many-to-many: each matching pair is emitted. One department row matched by two employees yields two output rows — the count multiplies, it doesn't collapse.
In a SELF JOIN Employees a JOIN Employees b ON a.dept_id=b.dept_id, does Cara (dept NULL) pair with herself?
No. Her key is NULL, and NULL = NULL is unknown, so she matches no row — not even her own. NULL-keyed rows silently disappear from equality self-joins.
FULL OUTER JOIN of our tables — how many rows, and which are NULL-padded?
Four rows: Asha–Sales (matched), Bilal–NULL, Cara–NULL (left orphans, right padded), NULL–Legal (right orphan, left padded). It is LEFT ∪ RIGHT with the single overlap counted once.
What happens to duplicate rows in a CROSS JOIN if the left table has repeated values?
Every physical left row is paired with every right row — duplicates are preserved and multiplied. CROSS operates on rows, not distinct values, so 2 identical left rows × 3 right rows still gives 6 output rows.
If you GROUP BY after a LEFT JOIN with unmatched (NULL) rows, how are the NULLs treated?
The NULL-padded rows still participate in grouping, but aggregates like COUNT(right_column) skip NULLs while COUNT(*) counts the padded row. This distinction is a classic trap — see GROUP BY and Aggregation.
Recall One-line self-test

Cover every reveal above and re-answer. Any item where your reason was fuzzy — even with the right verdict — is the one to redo tomorrow.


Back to the parent topic.