Ask the database, get the answer.
Most of an analyst’s questions are a JOIN, a GROUP BY and a filter. Here they are on a small shop’s real tables, run on PostgreSQL, with the traps interviewers love: missing matches, NULLs and ties.
- Choose between INNER and LEFT JOIN
- Group, filter groups and rank within them
- Count correctly around NULLs
You need: Lesson 1, or a little SQL.
- 1Statistics you use every day
- 2SQL that answers questions
- 3Charts that answer the question
- 4Power BI’s filter context
- 5A/B tests
- 6Guesstimates
-
JOIN: who matches
Predict first
The shop has 7 customers and 11 orders. An INNER JOIN of the two returns how many rows?
Just show me
11: one row for each order with a matching customer. Customers with no orders vanish.
An INNER JOIN keeps only the rows that match on both sides. That’s right for “each order with its customer’s name”, and wrong for “every customer and what they bought”, because customers who never ordered disappear without a warning.
SELECT c.name, o.order_id, o.amount FROM customers c JOIN orders o ON o.customer_id = c.customer_id ORDER BY o.order_idname order_id amount Aarav Sharma 1001 1798.00 Diya Patel 1002 2499.00 Aarav Sharma 1003 1999.00 Kabir Singh 1004 3398.00 Meera Iyer 1005 1299.00 The first 5 of 11 rows, as PostgreSQL returned them
Quick check
You want every customer, including those who never ordered. Which join?
A LEFT JOIN keeps every row of the left table and fills NULLs where nothing matches.
-
LEFT JOIN: who’s missing
Predict first
How many of the 7 customers never ordered?
Just show me
2: Aarav Sharma and Sana Khan. That Aarav Sharma is a second one, with the same name and the same email typed in capitals: a duplicate customer.
LEFT JOIN, count the orders, and the customers with none appear. Count
o.order_id, not*: COUNT(*) would count the empty match as an order. And the duplicate is a classic: real tables are full of the same person entered twice, which splits their history in two.SELECT c.name, COUNT(o.order_id) AS orders FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id GROUP BY c.customer_id, c.name ORDER BY orders DESC, c.namename orders Diya Patel 4 Aarav Sharma 3 Kabir Singh 2 Meera Iyer 1 Rohan Das 1 Aarav Sharma 0 Sana Khan 0 Seven rows, as PostgreSQL returned them
Run these yourself in the SQL Lab
Quick check
Why COUNT(o.order_id) and not COUNT(*) after a LEFT JOIN?
COUNT(column) skips NULLs; COUNT(*) counts rows, the empty match included.
-
GROUP BY, then HAVING
Predict first
Which payment mode brought in the most delivered revenue?
Just show me
UPI: ₹10,591 from 6 orders, against ₹3,998 by card and ₹2,499 cash on delivery.
WHERE filters rows before grouping (here, delivered orders only); GROUP BY makes one row per group; HAVING filters the groups afterwards. “Customers with 2 or more orders” is HAVING COUNT(*) >= 2, which finds 3 customers here. Mixing up WHERE and HAVING is one of the most-asked SQL interview questions.
SELECT payment_mode, COUNT(*) AS orders, SUM(amount) AS revenue FROM orders WHERE status = 'delivered' GROUP BY payment_mode ORDER BY revenue DESCpayment_mode orders revenue UPI 6 10591.00 Card 2 3998.00 COD 1 2499.00 Three rows, as PostgreSQL returned them
Quick check
Cities with more than 100 orders: where does “more than 100” go?
It’s a condition on each group’s count, so it filters groups: HAVING.
-
Ranking within groups
Predict first
In Analytics, two people earn the same. With RANK(), what rank comes after their shared 3rd?
Just show me
5: Priya Nair and Vikram Joshi share 3rd, so RANK skips 4. DENSE_RANK would give 4, and ROW_NUMBER would break the tie.
A window function computes across related rows without collapsing them: PARTITION BY says which rows belong together, ORDER BY sets the order inside. The top 3 earners in each department, each customer’s latest order and a running total are all window functions.
SELECT name, department, salary, RANK() OVER ( PARTITION BY department ORDER BY salary DESC ) AS rnk FROM employees ORDER BY department, rnkname department salary rnk Nisha Rao Analytics 3200000 1 Arjun Mehta Analytics 1800000 2 Priya Nair Analytics 1500000 3 Vikram Joshi Analytics 1500000 3 Farah Ali Analytics 1200000 5 Kunal Verma Operations 1600000 1 Ishaan Gupta Operations 1400000 2 Tara Bose Operations 1100000 3 Eight rows, as PostgreSQL returned them
Quick check
You want exactly one row per customer: their latest order. Which function?
ROW_NUMBER never ties, so “= 1” picks exactly one row in each partition.
-
NULL is not a value
Predict first
7 customers. What does COUNT(referred_by) return?
Just show me
3: COUNT(column) skips NULLs, and only 3 customers were referred by someone.
NULL means unknown, so
referred_by = NULLis never true: writeIS NULL. Aggregates skip NULLs (AVG divides by the non-NULL count only), and COALESCE fills them when a default makes sense. Most wrong totals in SQL come from forgetting this.SELECT COUNT(*) AS all_rows, COUNT(referred_by) AS referred FROM customersall_rows referred 7 3 One row, as PostgreSQL returned it
Quick check
WHERE referred_by = NULL returns nothing. The fix?
A comparison with NULL is unknown, never true; IS NULL is the test.
Lesson 2 done.
Next: Charts that answer the question. A chart is an answer to a question.
In the interview: SQL that answers questions.
MediumExplain INNER, LEFT, RIGHT and FULL OUTER joins
MediumTwo tables have repeated keys and a NULL key. How many rows does each type of join return?
EasyWhat is the difference between WHERE and HAVING?
MediumWhat types of window functions does SQL have?
EasyWhy does WHERE referred_by = NULL return nothing?
MediumHow do you find duplicate rows with a window function?
Ask Prism about this lesson
Sources
- The SQL guide’s example shop, every query here run on PostgreSQL (PGlite, the SQL Lab’s engine) on 2 Nov 2026.
- PostgreSQL documentation, “Window Functions”.