Hook: Interviewers love LEFT JOIN because it tests whether you understand how SQL keeps rows even when the other side has no match.
Question: What is a LEFT JOIN in SQL?
Answer: A LEFT JOIN returns every row from the left table and matches rows from the right table when the join condition is true. If there is no matching row on the right, SQL still keeps the left row and fills the right-side columns with NULL values. This is why it is useful when you want a complete list of things from one table, plus optional related data from another table.
Interview-Ready Answer: I use a LEFT JOIN when I want all rows from the left table, even if there is no matching row on the right. The right-side columns come back as NULL when there is no match. A classic example is listing all customers and showing their orders if they have any, while still keeping customers with zero orders.
Detailed Explanation: A LEFT JOIN is a way to combine rows from two tables using a matching rule, usually a key like customer_id or user_id. The word left means the table written first in the query. The database guarantees that every row from that first table appears in the result, even if the second table has no corresponding row.
ON condition.NULL values in that no-match case.This is why a LEFT JOIN can increase row count: one left row may match many right rows, so the result can repeat the left row multiple times. The join condition matters a lot. If you join on a non-unique column, you can create duplicates that are correct by SQL rules but surprising in reports.
| Join | Left rows kept? | Right rows kept? | No match behavior |
|---|---|---|---|
| INNER JOIN | No | No | Row disappears |
| LEFT JOIN | Yes | Only matches | Right side becomes NULL |
| RIGHT JOIN | Only matches | Yes | Left side becomes NULL |
Most teams prefer LEFT JOIN over RIGHT JOIN because it reads more naturally: start with the main entity you care about, then attach optional data. In practice, a RIGHT JOIN can usually be rewritten as a LEFT JOIN by swapping table order.
A very common gotcha is putting filters on the right table in the WHERE clause. Because WHERE runs after the join, a condition like WHERE orders.status = 'paid' removes the NULL rows and can accidentally turn the query into an inner join. If you want to keep unmatched left rows, put the filter in the ON clause instead.
The theoretical cost depends on the database plan, not just the SQL keyword. With indexes on the join keys, many engines can do the join efficiently, often close to O(n log m) or better depending on the strategy. Without indexes, a naive nested-loop style plan can behave like O(n × m). On real systems, a join between a million rows and a million rows can be fast with good indexing, but painfully slow without it.
Important details interviewers like:
NULL means “unknown or missing,” not zero or empty string.WHERE right_table.id IS NULL.EXISTS is clearer and can be more efficient.Think: “Keep the guest list, even if the plus-one never showed up.” The left table is the guest list; the right table is the optional plus-one details.
Real-World Example: Imagine a checkout service for an e-commerce app. The product team wants a dashboard that lists every customer, including people who never ordered, and shows the date of their latest order if they have one. A LEFT JOIN is the natural choice because the customer list must stay complete.
A developer writes a LEFT JOIN, then adds WHERE orders.status = 'paid'. Suddenly customers with no orders disappear from the report. The bug shows up as lower-than-expected customer counts, support tickets from analysts asking why inactive customers vanished, and logs that look “fine” because the SQL returns valid rows. The outage is not a crash; it is a silent correctness bug, which is often worse because dashboards and billing reports can be trusted incorrectly.
In production, that can mean a retention report undercounts new users, a finance job misses unpaid accounts, or a CRM export fails to include customers who need follow-up. The fix is usually to move the filter into the ON clause or to intentionally use a different join if unmatched rows should be removed.
-- LEFT JOIN demo: keep all customers, even those without orders.
-- This script is written in standard SQL style and runs in SQLite, PostgreSQL, and many other databases.
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
customer_name VARCHAR(50) NOT NULL
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
order_total DECIMAL(10,2),
status VARCHAR(20) NOT NULL
);
INSERT INTO customers (customer_id, customer_name) VALUES
(1, 'Ava'),
(2, 'Ben'),
(3, 'Chen');
INSERT INTO orders (order_id, customer_id, order_total, status) VALUES
(101, 1, 49.99, 'paid'),
(102, 1, 19.50, 'pending'),
(103, 2, 75.00, 'paid'),
(104, NULL, 12.00, 'paid'); -- edge case: an order without a valid customer_id
-- 1) Basic LEFT JOIN: every customer appears.
SELECT
c.customer_id,
c.customer_name,
o.order_id,
o.order_total,
o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
ORDER BY c.customer_id, o.order_id;
-- 2) Find customers with NO orders.
-- The NULL test works because unmatched right-side columns are filled with NULL.
SELECT
c.customer_id,
c.customer_name
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL
ORDER BY c.customer_id;
-- 3) Common mistake: this WHERE clause removes NULL-matched rows
-- and effectively behaves like an INNER JOIN for paid orders only.
SELECT
c.customer_id,
c.customer_name,
o.order_id,
o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = 'paid'
ORDER BY c.customer_id, o.order_id;
-- 4) Correct way to keep all customers while only attaching paid orders.
SELECT
c.customer_id,
c.customer_name,
o.order_id,
o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = 'paid'
ORDER BY c.customer_id, o.order_id;Follow-up & Tricky Questions:
LEFT JOIN and then filter with WHERE right_table.id IS NULL. That gives you the anti-join pattern, which is common in audits and cleanup jobs.WHERE right_table.col = ... dangerous after a LEFT JOIN? Because it removes rows where the right side is NULL, which defeats the point of keeping unmatched left rows. Put the condition in ON if you want unmatched left rows preserved.LEFT JOIN with EXISTS? Sometimes yes, especially when you only need to test presence. EXISTS is often clearer for boolean checks, while LEFT JOIN is better when you need columns from both tables.LEFT JOIN the same as LEFT OUTER JOIN? Yes. OUTER is optional keyword noise in SQL syntax; LEFT JOIN and LEFT OUTER JOIN mean the same thing.Tricky / gotcha questions:
WHERE, is it still a left join? Not logically for the result you get. The join still executes, but the WHERE clause can remove the unmatched rows, making it behave like an inner join for that condition.LEFT JOIN guarantee one row per left row? No. It guarantees at least one output row per left row, but if there are multiple matches on the right, the left row is duplicated.NULL join keys match each other? No, not with normal equality joins. NULL means unknown, so NULL = NULL is not true; unmatched rows remain unmatched unless the database offers special null-safe syntax.Common Mistakes:
WHERE instead of ON — This can erase the very unmatched rows you wanted to keep. Move right-table filters into ON when preserving all left rows matters.DISTINCT carefully.NULL with zero — NULL means missing, not a real value. Use COALESCE if you want a display default like 0 or 'none'.Memory Hook: “Keep the left side on the guest list, even if the right side no-shows.”
Cheat Sheet:
LEFT JOIN keeps all rows from the left table.NULL.ON for matching rules; use WHERE for final filtering.WHERE right.id IS NULL finds missing matches.LEFT JOIN and LEFT OUTER JOIN mean the same thing.Practice Tasks:
LEFT JOIN and a NULL check.