SQL interview questions and answers

SQL interview questions and answers

Updated on

Here are the SQL questions that come up most often in job interviews for data analyst, developer or data engineer roles, each with a short answer and, where needed, a worked query. The queries are written in SQLite and were run on the tables below; they also work in PostgreSQL and MySQL 8.

The tables used

The practical questions use three SpeedQL tables: employees (staff, with their manager’s id), customers and their orders (customers, orders), and sales (sales).

Table staff (10 rows)
idnamedepartmentsalarymanager_idhired
1AliceFinance52000NULL2015-03-01
2BobIT4100012018-06-15
3ClaireFinance3800012019-01-10
4DavidHR2900012020-09-01
5EmmaIT5000022017-11-20
6FaridIT4700022021-02-14
7GaelleHR3300042022-05-30
8HugoFinance4400032016-08-08
9IrisIT4700052023-01-09
10JulesFinance4400032024-03-18
Table customers (4 rows)
idnamecity
1AliceParis
2BrunoLyon
3ChloeParis
4DylanNantes
Table orders (6 rows)
idcustomer_idamount
11120
2180
32200
4350
5375
6330
Table sales (12 rows)
idsellerregionmonthamount
1AnaNorth2025-01300
2AnaNorth2025-02450
3AnaNorth2025-03400
4BenNorth2025-01500
5BenNorth2025-02350
6BenNorth2025-03600
7CleoSouth2025-01200
8CleoSouth2025-02700
9CleoSouth2025-03650
10DanSouth2025-01400
11DanSouth2025-02400
12DanSouth2025-03100

WHERE or HAVING: which one should filter?

WHERE filters rows before grouping; HAVING filters groups after GROUP BY. So a condition on an aggregate (COUNT, SUM, AVG…) goes in HAVING, and a condition on an ordinary column goes in WHERE, which is also faster.

Review: the HAVING card and the HAVING exercises with solutions.

What is the difference between INNER JOIN and LEFT JOIN?

INNER JOIN keeps only the rows that have a match in both tables. LEFT JOIN keeps every row of the left table and fills the right-hand columns with NULL when there is no match. Use LEFT JOIN when you must not lose any row of the first table, for example to list every customer, including those with no orders.

Review: the JOIN exercises with solutions.

What is the difference between UNION and UNION ALL?

UNION stacks the results of two queries and removes duplicates; UNION ALL keeps every row, duplicates included, and is faster because it does not have to remove them. Here, the 10 employees belong to 3 departments: UNION returns 3 rows, UNION ALL 20.

SELECT COUNT(*)
FROM (SELECT department FROM staff UNION SELECT department FROM staff)
UNION ALL SELECT COUNT(*)
FROM (SELECT department FROM staff UNION ALL SELECT department FROM staff);
Result
COUNT(*)
3
20

What is the difference between DELETE, TRUNCATE and DROP?

DELETE removes rows, all of them or only those matching a WHERE, one by one; TRUNCATE empties the whole table at once, much faster, with no condition allowed; DROP removes the table itself, structure included. SQLite has no TRUNCATE: a DELETE without WHERE does that job.

What are a primary key and a foreign key?

The primary key identifies each row uniquely and cannot be NULL: staff.id, for example. A foreign key is a column that refers to the primary key of another table (or the same one): orders.customer_id points to customers.id, and staff.manager_id to staff.id. It guarantees you cannot create an order for a customer who does not exist.

What is an index for, and when should you create one?

An index is a sorted structure, like the index of a book, that finds rows without reading the whole table. It speeds up WHERE, joins and ORDER BY on the indexed columns, but slightly slows down INSERT and UPDATE and takes up space. So you index the columns you often filter or join on, such as foreign keys.

How does NULL behave, and what does COUNT count?

NULL means “unknown value”: any comparison with NULL, even NULL = NULL, is neither true nor false. So you test IS NULL or IS NOT NULL. COUNT(*) counts every row, while COUNT(column) skips NULLs: here, 10 employees, 9 of whom have a manager.

SELECT COUNT(*) AS rows_count, COUNT(manager_id) AS with_manager
FROM staff;
Result
rows_countwith_manager
109

In what order is a SQL query executed?

The logical order is not the order it is written in: FROM and JOIN, then WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY and finally LIMIT. That is why, in standard SQL, an alias defined in SELECT cannot be used in WHERE, but can be used in ORDER BY.

ROW_NUMBER, RANK or DENSE_RANK: what is the difference?

All three number the rows and only differ on ties. ROW_NUMBER always gives a unique number; RANK gives tied rows the same rank, then skips ranks; DENSE_RANK gives the same rank with no gap. Look at Farid and Iris, tied at 47,000.

SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn, RANK() OVER (ORDER BY salary DESC) AS rk, DENSE_RANK() OVER (ORDER BY salary DESC) AS drk
FROM staff;
Result
namesalaryrnrkdrk
Alice52000111
Emma50000222
Farid47000333
Iris47000433
Hugo44000554
Jules44000654
Bob41000775
Claire38000886
Gaelle33000997
David2900010108

Review: the window function exercises with solutions.

What is database normalisation?

It is the way tables are organised to avoid repetition and inconsistencies. In practice: one value per cell (first normal form), every column depends on the whole key (second), and not on another non-key column (third). Instead of repeating the customer’s name and city in every order, you store them once in customers and link orders through customer_id.

How do you find the second highest salary?

The classic answer: take the highest salary among those lower than the maximum. This version handles ties and returns NULL if there is only one salary.

SELECT MAX(salary) AS second_salary
FROM staff
WHERE salary < (SELECT MAX(salary) FROM staff);
Result
second_salary
50000

Another way, with DISTINCT so the same salary is not counted twice:

SELECT DISTINCT salary
FROM staff
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

How do you find duplicate values in a column?

Group on the column and keep the groups that have more than one row, with HAVING COUNT(*) > 1.

SELECT department, COUNT(*) AS n
FROM staff
GROUP BY department
HAVING COUNT(*) > 1;
Result
departmentn
Finance4
HR2
IT4

How do you list employees who earn more than their manager?

It is a self-join: you join the staff table to itself, once for the employee (e) and once for their manager (m), then compare the two salaries.

SELECT e.name, e.salary, m.name AS manager, m.salary AS manager_salary
FROM staff e
JOIN staff m ON m.id = e.manager_id
WHERE e.salary > m.salary;
Result
namesalarymanagermanager_salary
Emma50000Bob41000
Farid47000Bob41000
Gaelle33000David29000
Hugo44000Claire38000
Jules44000Claire38000

How do you find customers who have never ordered?

With a LEFT JOIN, customers without orders have NULL in the orders columns: keep them with IS NULL.

SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
Result
name
Dylan

Same result with NOT EXISTS, often more readable and safer than NOT IN when the subquery may contain NULLs:

SELECT name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

How do you get the top row of each group?

Number the rows within each group with ROW_NUMBER() OVER (PARTITION BY …), in a CTE, then keep number 1. For the top N, just write n <= N.

WITH r AS (
  SELECT name, department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS n
  FROM staff)
SELECT department, name, salary
FROM r
WHERE n = 1;
Result
departmentnamesalary
FinanceAlice52000
HRGaelle33000
ITEmma50000

How do you compute a running total and a share of the total?

A window function computes over a set of rows without grouping them. SUM() OVER (ORDER BY …) gives a running total:

SELECT month, SUM(amount) AS total, SUM(SUM(amount)) OVER (ORDER BY month) AS running_total
FROM sales
GROUP BY month
ORDER BY month;
Result
monthtotalrunning_total
2025-0114001400
2025-0219003300
2025-0317505050

And SUM() OVER () (an empty window) gives the grand total, used to compute a percentage share:

SELECT region, SUM(amount) AS total, ROUND(100.0 * SUM(amount) / SUM(SUM(amount)) OVER (), 1) AS pct
FROM sales
GROUP BY region;
Result
regiontotalpct
North260051.5
South245048.5

Practice for your interview

The most effective preparation is still writing queries, fast and often. SpeedQL makes you write real queries against the clock, checked straight away.

Start a timed game

The 196 SQL exercises with solutions · Learn SQL: where to start