SQL interview practice: queries, sample data and answers.
A useful SQL answer includes more than a query that runs. Define what one result row represents, explain which records qualify, and check a case that could expose a mistake. Work through this original dataset to practise that reasoning.
Start with two small tables
Each customer row identifies one customer. Each order row identifies one order and points to a customer through customer_id. A customer can have several orders or none. Amounts are whole rupees in this teaching dataset, and status tells us whether an order is paid or cancelled.
Run the setup below in a fresh SQLite database if you want to execute the examples. These lesson tables are independent of the InterviewSkills exercise database. First inspect the rows: Asha has two paid orders, Bilal has one paid and one cancelled order, Chen has no order, and Diya has one paid order.
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
amount INTEGER NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
INSERT INTO customers VALUES
(1, 'Asha'), (2, 'Bilal'), (3, 'Chen'), (4, 'Diya');
INSERT INTO orders VALUES
(101, 1, 600, 'paid'),
(102, 1, 400, 'paid'),
(103, 2, 800, 'paid'),
(104, 2, 300, 'cancelled'),
(105, 4, 200, 'paid');| customer_id | name |
|---|---|
| 1 | Asha |
| 2 | Bilal |
| 3 | Chen |
| 4 | Diya |
| order_id | customer_id | amount | status |
|---|---|---|---|
| 101 | 1 | 600 | paid |
| 102 | 1 | 400 | paid |
| 103 | 2 | 800 | paid |
| 104 | 2 | 300 | cancelled |
| 105 | 4 | 200 | paid |
Filter individual orders with WHERE
Question: Which paid orders have an amount of at least 500? One result row should represent one order. Both conditions apply to that order, so combine them with AND in WHERE. The cancelled order is excluded by its status; the smaller paid orders are excluded by their amounts.
Predict the identifiers before running the query, then compare your answer with the result.
SELECT order_id, customer_id, amount
FROM orders
WHERE status = 'paid' AND amount >= 500
ORDER BY order_id;| order_id | customer_id | amount |
|---|---|---|
| 101 | 1 | 600 |
| 103 | 2 | 800 |
Why this works
Orders 101 and 103 qualify. The comparison uses >= because “at least” includes equality. ORDER BY makes the displayed order explicit, which matters whenever a question asks for ordered results.
Filter customer totals with HAVING
Question: Which customers have a paid-order total of at least 900? Now one result row represents a customer’s paid total. WHERE first keeps paid orders; GROUP BY gathers them by customer; SUM calculates each total; HAVING selects the qualifying groups.
SELECT customer_id, SUM(amount) AS paid_total
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING SUM(amount) >= 900
ORDER BY customer_id;| customer_id | paid_total |
|---|---|
| 1 | 1000 |
Why this works
Asha qualifies because 600 + 400 = 1,000. Neither individual order reaches 900. Filtering with WHERE amount >= 900 would remove both rows before they could contribute to the total. Bilal’s cancelled 300 does not count, so his paid total is 800.
Keep customers with no paid orders
Question: Show every customer, their paid-order count and their paid total. Start from customers and LEFT JOIN qualifying orders. Put the paid-status condition in ON so it controls which orders match while preserving every customer.
SELECT c.customer_id, c.name,
COUNT(o.order_id) AS paid_orders,
COALESCE(SUM(o.amount), 0) AS paid_total
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = 'paid'
GROUP BY c.customer_id, c.name
ORDER BY c.customer_id;| customer_id | name | paid_orders | paid_total |
|---|---|---|---|
| 1 | Asha | 2 | 1000 |
| 2 | Bilal | 1 | 800 |
| 3 | Chen | 0 | 0 |
| 4 | Diya | 1 | 200 |
Why this works
Chen receives an unmatched joined row with NULL order fields. COUNT(o.order_id) counts only non-NULL order identifiers, giving Chen zero. COUNT(*) would count the unmatched row and incorrectly report one order. SUM has no amount to add for Chen, so COALESCE turns that NULL result into the report’s requested zero.
Grouping by both the identifier and name keeps the query’s intended grouping clear. A name alone is a poor identifier: two different customers can share it.
Find missing matches with IS NULL
Question: Which customers have no paid order? Reuse the paid-order LEFT JOIN, then keep the rows whose order identifier is NULL. Because order_id is a primary key, an actual matching order cannot have a NULL identifier. The NULL therefore identifies a missing match.
SELECT c.customer_id, c.name
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = 'paid'
WHERE o.order_id IS NULL
ORDER BY c.customer_id;| customer_id | name |
|---|---|
| 3 | Chen |
Why this works
Use IS NULL for this test. Writing = NULL does not produce a true comparison for these rows. Also keep the meaning precise: a customer with cancelled orders only should qualify for “no paid order”, even though they have placed an order.
Change the data to test your explanation
Add Esha with one cancelled order using the statements below. Before rerunning the previous queries, predict the changes. The every-customer report should gain Esha with zero paid orders and a zero paid total. The no-paid-order query should now return Chen and Esha.
INSERT INTO customers VALUES (5, 'Esha');
INSERT INTO orders VALUES (106, 5, 900, 'cancelled');Why this works
Try moving the paid-status condition from ON to WHERE in the every-customer report. Chen’s missing order produces a NULL status, while Esha’s matched order has the status cancelled. Neither satisfies WHERE o.status = 'paid', so both customers disappear. This small change in data exposes a mistake that a dataset containing only paying customers would hide.
Finally, change the totals question to “at least 800”. Asha and Bilal should qualify, with totals 1,000 and 800. Explain why replacing >= with > would exclude Bilal. Predicting these changes makes your reasoning easier to check than repeatedly editing a query until the sample output looks right.
- State what one output row represents before writing a join.
- Check whether conditions apply to source rows, matching rows or grouped totals.
- Test a missing match, repeated customer and exact boundary value.
- Explain each returned row and use explicit ordering when the task requires it.
Common questions
Do these examples run in every SQL database?
They are written and checked for SQLite. The main query concepts are widely used, but data types, setup syntax and supported features can vary by database. Check the SQL dialect named in your interview or exercise.
Can I run the setup inside an InterviewSkills SQL exercise?
Use a separate fresh SQLite database for this lesson’s setup. InterviewSkills exercises provide their own tables and task requirements; write the query against the schema supplied by the selected exercise.
Continue with SQL practice
Create an account to continue with guided learning and practice.
Already have an account? Sign in