Skip to content
InterviewSkills

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 the lesson dataset in a fresh SQLite database
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');
customers: one row per customer
customer_idname
1Asha
2Bilal
3Chen
4Diya
orders: one row per order
order_idcustomer_idamountstatus
1011600paid
1021400paid
1032800paid
1042300cancelled
1054200paid

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.

Find qualifying paid orders
SELECT order_id, customer_id, amount
FROM orders
WHERE status = 'paid' AND amount >= 500
ORDER BY order_id;
Expected result
order_idcustomer_idamount
1011600
1032800

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.

Aggregate before testing the total
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;
Expected result
customer_idpaid_total
11000

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.

Report every customer, including zero matches
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;
Expected result
customer_idnamepaid_orderspaid_total
1Asha21000
2Bilal1800
3Chen00
4Diya1200

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.

Find customers without a paid order
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;
Expected result
customer_idname
3Chen

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.

Add a customer with a cancelled order only
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.

Create account