Skip to content

Grokem · https://www.grokem.com/career/interview-preparation/sql-interview-questions-worked

Six SQL interview questions, worked through

Second-highest salary, top per group, rows with no match, comparing to a manager, month over month change and filtering groups, each with a tested solution and the reasoning to say out loud.

Grokem Editorial

Editorial desk

Published
  • Intermediate
  • 20 min to complete
Tested withSQLite3.42

Before you start

  • Comfort with SELECT, JOIN, GROUP BY and subqueries
  • A SQL environment to run the examples; they use SQLite, and the solutions are standard SQL

SQL interviews tend to circle the same handful of patterns. Learn the patterns and the questions stop being new. These six are worked on a small dataset; each solution was run and the output is real.

The data

sql
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT, salary INTEGER, manager_id INTEGER);
INSERT INTO employees VALUES
 (1, 'Ana', 'Data', 90, NULL), (2, 'Ben', 'Data', 70, 1), (3, 'Cy', 'Data', 95, 1),
 (4, 'Di', 'Eng', 100, NULL),  (5, 'Eli', 'Eng', 80, 4),  (6, 'Fay', 'Eng', 60, 4),
 (7, 'Gus', 'Ops', 50, NULL);

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers VALUES (1, 'Ana'), (2, 'Ben'), (3, 'Cy'), (4, 'Di');

CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, ordered_on TEXT, total INTEGER);
INSERT INTO orders VALUES
 (1, 1, '2025-01-05', 50), (2, 1, '2025-01-20', 70), (3, 2, '2025-02-01', 20),
 (4, 3, '2025-02-02', 90), (5, 3, '2025-03-01', 10);

1. The second-highest salary

sql
SELECT MAX(salary) AS second_highest
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

Result: 95. The inner query finds the top salary (100), and the outer takes the highest value below it. Say out loud: this treats equal salaries as one value, so it is the second-highest distinct salary. If two people tie for first and the question wants the second person, the answer changes, so ask. For the Nth highest, use DENSE_RANK() and filter on the rank.

2. The top earner in each department

sql
SELECT department, name, salary
FROM (
    SELECT *, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r
    FROM employees
)
WHERE r = 1
ORDER BY department;

Result: Cy (Data, 95), Di (Eng, 100), Gus (Ops, 50). PARTITION BY restarts the ranking for each department, and RANK() gives tied top earners all rank 1, so ties are kept. Use ROW_NUMBER() instead if you want exactly one row per department. The filter has to sit outside, because you cannot use a window function in WHERE.

3. Customers who never ordered

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

Result: Di. This is an anti-join. A left join keeps every customer, and customers with no matching order have NULL in the order columns, which the WHERE then selects. NOT EXISTS expresses the same thing and is just as good. Avoid NOT IN (subquery) when the subquery can return NULL, because it then returns nothing at all.

4. Employees who earn more than their manager

sql
SELECT e.name AS employee, e.salary, m.name AS manager, m.salary AS manager_salary
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
WHERE e.salary > m.salary;

Result: Cy earns 95, more than Ana (90). A self-join joins a table to itself under two aliases, one playing the employee and one the manager. The inner join drops people with no manager, which is right here. Say so.

5. Month-over-month change

sql
WITH monthly AS (
    SELECT substr(ordered_on, 1, 7) AS month, SUM(total) AS revenue
    FROM orders
    GROUP BY 1
)
SELECT month, revenue, revenue - LAG(revenue) OVER (ORDER BY month) AS change
FROM monthly
ORDER BY month;
month revenue change
2025-01 120 NULL
2025-02 110 -10
2025-03 10 -100

LAG reaches back one row in the ordered result. The first month has nothing before it, so change is NULL. Mention: a month with no orders would be missing from the result rather than shown as zero, and the "previous" row would then be two months back. For reports, join to a calendar table.

6. Departments with a high average salary

sql
SELECT department, ROUND(AVG(salary), 1) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 75;

Result: Data 85.0 and Eng 80.0. WHERE filters rows before grouping, and HAVING filters groups after, which is the only place you can test an aggregate. Ops (50) is excluded.

The pattern behind them

If the question says... Reach for
"Nth highest", "top per group", "rank" DENSE_RANK / RANK / ROW_NUMBER with PARTITION BY
"who has no ...", "never" Anti-join: LEFT JOIN ... IS NULL or NOT EXISTS
"compare a row with its parent or related row" Self-join
"change from the previous ...", "running total" LAG / LEAD, SUM() OVER
"groups where the total or average ..." GROUP BY ... HAVING
"duplicates" GROUP BY ... HAVING COUNT(*) > 1, or ROW_NUMBER

How to practise

  1. Do each problem on paper first, then run it. Predict the result before you press enter.
  2. Change the data: add a tie, add a NULL, add an empty group. See which solutions break, and fix them.
  3. Explain each query to someone in two sentences. If you cannot, you have not finished understanding it.
  4. Time yourself, but aim first for correct and clear, then for fast.

Expected result

Each query returns the result shown, and you can explain why it works and what would change if the data had ties or missing values.

Frequently asked questions

Do interviewers want the cleverest query?

No. They want a correct one, explained clearly, with the edge cases noticed. A simple, readable answer that handles ties and nulls beats a clever one-liner you cannot explain.

What should I say while solving?

Restate the question, ask about ties, duplicates and nulls, sketch the approach, then write the query and check it against a tiny example.

References

From Grokem: https://www.grokem.com/career/interview-preparation/sql-interview-questions-worked

How did this strike you?

Be the first to react – it is anonymous.

Community

Questions about this?

Ask in Careers and learning and get help from other readers.

Ask a questionBrowse Careers and learning

Comments (2)

  • Nandini R.

    In question 4, why does NOT IN return nothing when the list contains a NULL?

    • Grokem Editorial

      `x NOT IN (1, NULL)` means `x <> 1 AND x <> NULL`, and a comparison with `NULL` is neither true nor false but unknown, so the whole condition is never true. `NOT EXISTS` does not have this trap, which is why it is usually the safer way to write it.

Choose what we may use. You can change this at any time with “Privacy choices” at the foot of every page. Details are in the Cookie policy and the Privacy policy.

Strictly necessary

Remembers your theme, the lessons you marked as done and this choice. It stays on your device, tracks nothing and cannot be switched off.

Always on
What exactly is collected, and why

Grokem’s own counts

Collects
The pages you open and how long you stay, how far you scroll, where you came from, your device type, browser, screen size, language and time zone. Your IP address is turned into a code that changes every day; it is never stored as it is.
Why
To see how long lessons are read and where readers leave. Not shared with anyone.
Stored
A random visitor ID, a visit ID and the time of your last page view in your browser’s storage (gr_vid, gr_sid, gr_last). No name or email is attached.