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);
idname
37Dmitri Rahimi
38Callum Kowalski
39Thea Sorensen
40Rafi 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;
deptsalarypeople
eng1310002
sales880002
support610002

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;
employeesalarymanagermanager_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_cancelledpct_shipped
11.282.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;
monthorderschange
2025-025NULL
2025-03105
2025-048-2
2025-0591
2025-06101
2025-078-2

6 rows

The rest of the list

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.