SQL guides · 2 min
SQL interview questions for data analysts, with runnable answers
Most SQL interview questions are a handful of patterns in different clothes. Here are the ones worth drilling, with what each is really testing. Try writing each one before you read the answer.
Find customers with no orders
Tests whether you know anti-joins and can avoid the NOT IN trap.
SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);| id | name |
|---|---|
| 37 | Dmitri Rahimi |
| 38 | Callum Kowalski |
| 39 | Thea Sorensen |
| 40 | Rafi Brandt |
4 rows
More in why NOT IN returns nothing.
Second-highest salary
Tests whether you think about ties. A follow-up usually asks for the Nth.
SELECT MAX(salary) AS second_highest
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);| second_highest |
|---|
| 152000 |
1 row
For the general Nth, use DENSE_RANK.
Duplicate rows
Tests GROUP BY with HAVING. Here, people who share a salary within a department:
SELECT dept, salary, COUNT(*) AS people
FROM employees
WHERE salary IS NOT NULL
GROUP BY dept, salary
HAVING COUNT(*) > 1;| dept | salary | people |
|---|---|---|
| eng | 131000 | 2 |
| sales | 88000 | 2 |
| support | 61000 | 2 |
3 rows
Employees who earn more than their manager
Tests self-joins.
SELECT e.name AS employee, e.salary, m.name AS manager, m.salary AS manager_salary
FROM employees e
JOIN employees m ON m.id = e.manager_id
WHERE e.salary > m.salary;| employee | salary | manager | manager_salary |
|---|
0 rows
No rows is the correct answer on this data. If the interviewer's data does return rows, the shape of the query doesn't change.
Percentage of orders in each status
Tests conditional aggregation and integer division.
SELECT ROUND(100.0 * SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_cancelled,
ROUND(100.0 * SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_shipped
FROM orders;| pct_cancelled | pct_shipped |
|---|---|
| 11.2 | 82.0 |
1 row
Write 100.0. With a plain 100, several databases do integer division.
Month-over-month change
Tests LAG and a clean grouping step.
WITH monthly AS (
SELECT substr(order_date, 1, 7) AS month, COUNT(*) AS orders
FROM orders
GROUP BY 1
)
SELECT month, orders, orders - LAG(orders) OVER (ORDER BY month) AS change
FROM monthly
ORDER BY month
LIMIT 6;| month | orders | change |
|---|---|---|
| 2025-02 | 5 | NULL |
| 2025-03 | 10 | 5 |
| 2025-04 | 8 | -2 |
| 2025-05 | 9 | 1 |
| 2025-06 | 10 | 1 |
| 2025-07 | 8 | -2 |
6 rows
The rest of the list
- Top N per group
- Longest streak of consecutive days
- Running totals
- Why a total is too high after a join
- Why a LEFT JOIN lost rows
- WHERE vs HAVING
Reading the answers doesn't help much with the interview itself. The hard part is producing the query cold while someone watches, so practice that.