SQL

  • Working with Data
  • 12 questions
  • 10 queries run on every build

What this round is for

SQL is the subject where a confident wrong answer survives longest, because the wrong answer usually returns rows. Nothing errors when a LEFT JOIN quietly becomes an inner one - the report is simply short, and nobody notices for a month.

Every query on this page is run against the same small database when the site is built, and the build fails if the result is not what the page claims. The tables are below, and they are small on purpose: one department with nobody in it, one employee with no department, and two people on the same salary.

The tables these queries run against

departments: Engineering, Sales, Legal. employees: Ama (Engineering, 90000), Bo (Engineering, 75000), Cai (Sales, 60000), Dev (Sales, 60000), Eli (no department, 50000).

The questions

Answer each one out loud before you open it. An answer you have read is not an answer you can give under pressure, which is the only kind that counts.

Q1 What is the difference between an INNER JOIN and a LEFT JOIN?

Show the answer Hide the answer

An inner join keeps only rows that match on both sides; a left join keeps every row from the left table, filling the right side with NULLs when nothing matches.

Here, Legal has no employees. The left join below still lists it, with a count of zero. Swap LEFT JOIN for JOIN and Legal vanishes from the report entirely - no error, just a department that silently stopped existing. That is the bug this question is about.

Run on every build, against the tables above:

SELECT d.name AS department, COUNT(e.id) AS people
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id
GROUP BY d.id, d.name
ORDER BY d.name
department people
Engineering 2
Legal 0
Sales 2

What it is really testing. Whether you have been caught by it. Everyone can recite the definition; the candidate who says "and that is how you lose the empty department" has debugged a report.

Q2 Why does COUNT(e.id) and not COUNT(*) matter in a LEFT JOIN?

Show the answer Hide the answer

COUNT(*) counts the row the join produced, so an unmatched department would count as one instead of zero.

A left join with no match still emits a row, with every right-hand column NULL. COUNT(*) counts rows and would report Legal as having one employee. COUNT(e.id) counts non-NULL values of that column, so it correctly reports zero. This is the single most common off-by-one in reporting SQL.

Run on every build, against the tables above:

SELECT COUNT(*) AS all_rows, COUNT(dept_id) AS rows_with_dept
FROM employees
all_rows rows_with_dept
5 4

What it is really testing. Whether you know COUNT ignores NULLs. It is the mechanism behind the previous answer and the next one, and it is worth saying out loud.

Q3 What is the difference between WHERE and HAVING?

Show the answer Hide the answer

WHERE filters rows before they are grouped; HAVING filters the groups afterwards.

That ordering is why you cannot put an aggregate in WHERE - at the point WHERE runs, the groups do not exist yet, so there is no average to compare against. Use WHERE to cut rows you never wanted, and HAVING only for conditions on the aggregate. Filtering early is also faster.

Run on every build, against the tables above:

SELECT d.name AS department, AVG(e.salary) AS avg_salary
FROM employees e
JOIN departments d ON d.id = e.dept_id
GROUP BY d.name
HAVING AVG(e.salary) > 70000
ORDER BY d.name
department avg_salary
Engineering 82500

What it is really testing. Whether you can say why, not just which. "HAVING is for aggregates" is correct and incomplete; the reason is the execution order.

Q4 In what order do the clauses of a SELECT actually run?

Show the answer Hide the answer

FROM and JOIN, then WHERE, then GROUP BY, then HAVING, then SELECT, then ORDER BY, then LIMIT.

Which is nearly the opposite of how you write it, and it explains two everyday puzzles: you cannot use a column alias defined in SELECT inside WHERE, because SELECT has not run yet - but you can use it in ORDER BY, because that runs after.

What it is really testing. Whether the alias rule is something you understand or something you have memorised as an arbitrary restriction. Expect the alias question as the follow-up.

Q5 What does a NULL do to a comparison?

Show the answer Hide the answer

Any comparison with NULL is unknown, not true - so rows with NULL are dropped by both = and !=.

Eli has no department. WHERE dept_id != 1 looks as though it should return everybody outside Engineering, which is three people, and it returns two - Eli is excluded, because NULL != 1 is not true. To include her you need OR dept_id IS NULL. This is the most expensive NULL mistake in everyday SQL.

Run on every build, against the tables above:

SELECT COUNT(*) AS not_in_engineering
FROM employees
WHERE dept_id != 1
not_in_engineering
2

What it is really testing. Whether you reach for IS NULL rather than = NULL, and whether you know that != excludes NULLs too. The second half catches most people.

Q6 How do you find the second-highest salary?

Show the answer Hide the answer

Order descending and skip one - with DISTINCT, so that a tie at the top does not cost you the answer.

DISTINCT is the part being tested. Without it, two people on the top salary would make the "second row" the same figure as the first. The window function DENSE_RANK() is the more general answer and the one to give if asked for the Nth.

Run on every build, against the tables above:

SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1
salary
75000

What it is really testing. Whether you handle ties. The naive LIMIT 1 OFFSET 1 is right only when the top salary is unique, and the interviewer will say "what if two people earn it?"

Q7 What does GROUP BY do with NULLs?

Show the answer Hide the answer

It puts them all in one group of their own, even though NULL does not equal NULL.

This is an exception worth knowing: in a comparison NULL is never equal to anything including itself, but GROUP BY, DISTINCT and UNION all treat NULLs as the same value for grouping. So the employee with no department becomes her own row in the result rather than disappearing.

Run on every build, against the tables above:

SELECT dept_id, COUNT(*) AS people, SUM(salary) AS total
FROM employees
GROUP BY dept_id
ORDER BY dept_id
dept_id people total
NULL 1 50000
1 2 165000
2 2 120000

What it is really testing. Whether you know the inconsistency exists. It genuinely is inconsistent, and candidates who have only read the comparison rule guess wrongly.

Q8 UNION or UNION ALL?

Show the answer Hide the answer

UNION removes duplicates; UNION ALL keeps everything, and is faster because of it.

Removing duplicates means sorting or hashing the whole result, which is real work. If you know the two sets cannot overlap - or you want the duplicates - UNION ALL is both correct and cheaper. Reaching for UNION by habit is a common and invisible performance cost.

Run on every build, against the tables above:

SELECT name FROM employees WHERE salary >= 75000
UNION ALL
SELECT name FROM employees WHERE dept_id = 1
ORDER BY name
name
Ama
Ama
Bo
Bo

What it is really testing. Whether you know UNION costs something. Most answers give the behavioural difference and stop short of the reason it matters.

Q9 How do you find duplicate values in a column?

Show the answer Hide the answer

Group by the column and keep the groups with more than one row.

GROUP BY collapses equal values into one row each, and COUNT(*) tells you how many went in. HAVING COUNT(*) > 1 then keeps only the ones that were duplicated. The same shape finds duplicate emails before you add a unique constraint, which is the real-world version of this question.

Run on every build, against the tables above:

SELECT salary, COUNT(*) AS people
FROM employees
GROUP BY salary
HAVING COUNT(*) > 1
salary people
60000 2

What it is really testing. Fluency with GROUP BY plus HAVING together. It is the most reused pattern in everyday SQL and a reliable warm-up question.

Q10 What is a window function, and how is it different from GROUP BY?

Show the answer Hide the answer

It computes across a set of rows without collapsing them - you keep every row and gain a column.

GROUP BY turns five employees into two department rows. A window function leaves all five and adds, say, their rank inside their department. That is what OVER (PARTITION BY ... ORDER BY ...) means: partition is the grouping, and the rows survive. Running totals and "rank within group" are the everyday uses.

Run on every build, against the tables above:

SELECT name,
       ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC, name) AS rank_in_dept
FROM employees
WHERE dept_id IS NOT NULL
ORDER BY dept_id, rank_in_dept
name rank_in_dept
Ama 1
Bo 2
Cai 1
Dev 2

What it is really testing. Whether you grasp that rows survive. "It is like GROUP BY but..." is the start of the right answer; the end of it is "without losing the rows".

Q11 When would you use a subquery rather than a join?

Show the answer Hide the answer

When you need a single aggregate to compare against, or you only want rows from one table.

Comparing each salary against the overall average is natural as a subquery and awkward as a join, because the average is one number rather than a table to match against. Where a join would work too, prefer it - a correlated subquery that re-runs per row is the classic accidental slow query.

Run on every build, against the tables above:

SELECT name
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY name
name
Ama
Bo

What it is really testing. Whether you know correlated subqueries can be expensive. The syntax half is easy; the performance half is what distinguishes the answers.

Q12 DELETE, TRUNCATE or DROP?

Show the answer Hide the answer

DELETE removes rows and can be filtered and rolled back; TRUNCATE empties the whole table quickly; DROP removes the table itself.

DELETE is logged row by row, so it is slow on big tables but works inside a transaction and takes a WHERE clause. TRUNCATE deallocates the data instead and usually resets auto-increment, but takes no WHERE and in many databases cannot be rolled back. DROP leaves no table at all.

What it is really testing. Whether you know which ones you can undo. This question exists because the wrong choice on a live database is unrecoverable.

Two things worth doing in the room

  • When you are given a query to write, say what the tricky row is first - the department with nobody in it, the NULL - and then write it. That is the thinking being marked.
  • If a question mentions a report that is "missing rows", the answer is almost always a join turned inner or a NULL comparison. Check those two before anything else.