SQL JOIN Interview Questions
Practice join keys, INNER vs LEFT JOIN, duplicate rows, and missing matches.
JOIN questions test whether you can reason about table relationships, not
just write join syntax. Interviewers want to hear which table
is the base, which key connects the tables, and whether unmatched rows
should remain in the result.
Start every join answer by naming the result grain. If the result should be one row per customer, do not let order or line-item rows accidentally change that grain. If the result should show every order line, a one-to-many join is expected.
Common JOIN interview prompts
Answer each one yourself before you look: write your query or your reasoning in the box, or open the playground and run it. Then reveal the model answer and compare. Nothing you type is saved or sent anywhere.
-
Return every customer and the number of orders they have placed, including customers with zero orders.
Try it in the playground →Reveal a model answer
select c.customer_id, c.first_name, c.last_name, count(o.order_id) as orders from customers as c left join orders as o on o.customer_id = c.customer_id group by c.customer_id, c.first_name, c.last_name order by orders desc, c.customer_idCustomers is the base table and the LEFT JOIN keeps the four who never ordered. Count o.order_id, not *: the unmatched row still exists, so count(*) would give those customers 1 instead of 0.
-
List each shipped order with its customer name and order total.
Try it in the playground →Reveal a model answer
select o.order_id, c.first_name || ' ' || c.last_name as customer_name, sum(oi.quantity * oi.unit_price) as order_total from orders as o join customers as c on c.customer_id = o.customer_id join order_items as oi on oi.order_id = o.order_id where o.status = 'shipped' group by o.order_id, customer_name order by o.order_idThe total lives on the line items, so the join fans each order out to one row per item, and GROUP BY order_id folds it back to one row per order. Say the grain out loud: one row per shipped order.
-
Find products that have never appeared in an order item.
Try it in the playground →Reveal a model answer
select p.product_id, p.product_name from products as p left join order_items as oi on oi.product_id = p.product_id where oi.product_id is null order by p.product_id
An anti-join: keep every product, then keep only those where the right side found nothing. NOT EXISTS answers it too, and says so more directly. Avoid NOT IN against order_items unless you know product_id is never null there.
-
Explain why a customer count becomes too high after joining orders to order_items.
Reveal a model answer
The join repeats each order once per line item, so each customer appears once per line item they bought. count(*) now counts line items. Count the entity you mean, count(distinct c.customer_id), or leave order_items out when the question does not need it.
-
Given rentals, customers, and movies, return who rented what, sorted by rental date.
Try it in the playground →Reveal a model answer
select r.rental_date, c.first_name, c.last_name, m.title from rentals as r join customers as c on c.customer_id = r.customer_id join movies as m on m.movie_id = r.movie_id order by r.rental_date, r.rental_idStart from the fact table, rentals, and join each lookup on its key. Add rental_id as a tie-breaker so two rentals on one day come back in the same order every time.
select c.customer_id,
c.first_name,
c.last_name,
count(o.order_id) as orders
from customers as c
left join orders as o on o.customer_id = c.customer_id
group by c.customer_id, c.first_name, c.last_name
order by orders desc, c.customer_id What matters
The first question needs a LEFT JOIN because customers with no
matching orders still belong in the output. count(o.order_id)
counts only matched orders; count(*) would count the preserved
customer row even when no order exists.
The more advanced version asks why totals inflate. Joining orders to order_items repeats each order once per line item. That is correct when the report is about line items, but wrong when the report is supposed to count orders or customers.
select o.order_id,
count(*) as joined_rows
from orders as o
join order_items as oi on oi.order_id = o.order_id
group by o.order_id
order by joined_rows desc How to talk it through
Talk through the grain first: "I want one row per customer, so customers is my base table. I need a left join to keep customers without orders. I will count order_id, not star, because the unmatched left-join row should count as zero."
Then add a quick check. Count rows before and after the join, inspect a few customers with multiple orders, and confirm whether a one-to-many join is expected. That habit is often more impressive than writing the final query immediately.
Practice next
Work through joining customers to orders, INNER vs LEFT JOIN, and the inflated counts after a JOIN mistake page. For a fresh dataset, try the movie rentals join lesson.
Quick answers
How should I start a SQL JOIN interview answer?
Start by naming the result grain, then choose the base table, join type, and ON keys before writing the query.
When should I use LEFT JOIN in an interview query?
Use LEFT JOIN when every row from the base table must remain, such as customers with zero orders or products with no sales.
Why do counts get inflated after a JOIN?
Counts inflate when a one-to-many join repeats the row you are trying to count. Check the grain and count the correct key, often with DISTINCT.
Related
Join two related tables on a key.
INNER, LEFT, RIGHT, FULL, and CROSS joins.
Join types and common join patterns.
One-to-many joins can inflate totals.
Qualify shared columns with a table alias.
Join through a bridge table.