SQL interview questions for data analysts: 16 questions by level
Practice SQL interview questions for data analysts with 16 questions, answers, and explanations from basic to advanced.
What this practice test checks
In data analyst interviews and take-home tests, SQL questions usually check whether you can read table structures, filter the right rows, join tables correctly, aggregate without mixing up groups, and spot common mistakes like duplicated rows or NULLs. Hiring teams also look for whether you understand what a query actually returns, not just whether you recognize the syntax.
This practice test is organized into three levels: basic, intermediate, and advanced. The basic questions focus on SELECT, WHERE, ORDER BY, and simple aggregates. The intermediate questions move into JOINs, GROUP BY with HAVING, and NULL handling. The advanced questions cover window functions, common table expressions (CTEs), deduplication, and row explosion from joins.
Each question shows the table structure and a few sample rows in a code block, then asks you to choose the query that returns the requested result or to identify what a query returns. Work through the question first without looking at the explanation, then check the answer and explanation to confirm your reasoning. That approach is closer to a real interview, where you need to think through the result set step by step.
How to use this test
If you get stuck, write out the intermediate result: filtered rows, grouped rows, joined rows, and final output. That habit will help you catch off-by-one filters, duplicate matches, and window-function mistakes before they cost you points.
Basic · 6 questions
- 1.
A data analyst is checking a small customer table.
customers +----+--------+--------+ | id | name | city | +----+--------+--------+1 Ava Miami 2 Ben Dallas 3 Cara Miami +----+--------+--------+Which query returns the names of customers who live in Miami, sorted alphabetically?
Show answer
Correct answer: SELECT name FROM customers WHERE city = 'Miami' ORDER BY name;
Learn more about SQLThis is correct because the WHERE clause filters rows to Miami before ORDER BY sorts the resulting names alphabetically. The tempting wrong choice is the Dallas filter, which would return Ben instead of Ava and Cara. The other options either place ORDER BY in the wrong position or filter on the wrong column/value, so they do not return the requested result.
- 2.
A report needs order IDs with amounts above 100.
orders +---------+-----------+--------+ | order_id| customer | amount | +---------+-----------+--------+101 Ava 80 102 Ben 120 103 Cara 150 +---------+-----------+--------+Which query returns the two qualifying order IDs?
Show answer
Correct answer: SELECT order_id FROM orders WHERE amount > 100;
Learn more about SQLThe correct query uses WHERE to filter row-level values before any aggregation, so it returns order IDs 102 and 103. HAVING is for grouped results, not this simple filter, and the amount >= 100 version would also include the row with amount 100 if it existed. The wrong column in the third option changes the condition to an order ID test, which is unrelated to the requested result.
- 3.
A team wants one row per customer with their total spend.
orders +----------+-------------+--------+ | order_id | customer_id | amount | +----------+-------------+--------+1 10 40 2 10 60 3 20 30 +----------+-------------+--------+Which query returns each customer_id with the sum of amount?
Show answer
Correct answer: SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id;
Learn more about GROUP BY and HAVINGThe correct query groups rows by customer_id and then sums amount within each group, producing one total per customer. The tempting wrong option with WHERE customer_id is invalid for this task because WHERE does not create groups and the condition is not a meaningful comparison. The other distractors either group by the wrong column or sum the wrong field, so they return incorrect aggregates.
- 4.
An analyst wants a running total by month for one store.
sales +----+-------+--------+ | id | month | amount | +----+-------+--------+1 Jan 50 2 Feb 30 3 Mar 20 +----+-------+--------+Which query returns a cumulative sum ordered by month?
Show answer
Correct answer: SELECT month, amount, SUM(amount) OVER (ORDER BY month) AS running_total FROM sales;
Learn more about SQL Window FunctionsThe correct query uses SUM(amount) OVER (ORDER BY month) to produce a running total that grows row by row in month order. The tempting wrong option with PARTITION BY month resets the total for each month, so it does not accumulate across rows. The plain SUM without a window returns a single total for the whole table, and OVER () also returns the same grand total on every row.
- 5.
A reporting query needs one row per department after filtering departments with at least two employees.
staff +----+----------+--------+ | id | dept | title | +----+----------+--------+1 Sales Rep 2 Sales Lead 3 HR Analyst 4 IT Admin +----+----------+--------+Which query returns only the departments with two or more staff members?
Show answer
Correct answer: SELECT dept, COUNT(*) FROM staff GROUP BY dept HAVING COUNT(*) >= 2;
Learn more about GROUP BY and HAVINGThis query is correct because GROUP BY dept creates department groups and HAVING COUNT(*) >= 2 filters those groups after aggregation. The tempting wrong option uses WHERE with COUNT(*), which is invalid because aggregates are evaluated after WHERE. The other options either group by the wrong column or place WHERE after GROUP BY, which is not valid SQL syntax for this task.
- 6.
A reporting team has these tables:
customers +----+--------+ | id | name | +----+--------+1 Ava 2 Ben 3 Cara +----+--------+ orders +----------+-------------+--------+ | order_id | customer_id | amount | +----------+-------------+--------+10 1 40 11 1 25 12 2 60 +----------+-------------+--------+Which query lists every customer and their orders, including customers with no orders?
Show answer
Correct answer: SELECT c.name, o.order_id FROM customers c LEFT JOIN orders o ON c.id = o.customer_id;
Learn more about SQL JoinsLEFT JOIN keeps all rows from customers and matches orders when a customer has them, so Ben and Cara still appear even if one has no matching order. INNER JOIN would drop customers with no orders. RIGHT JOIN keeps all orders instead, and CROSS JOIN would create every possible pair rather than matching by customer_id.
Intermediate · 3 questions
- 7.
A data analyst checks orders and wants to return only the two orders with the largest amounts. The table is:
orders +----------+-------------+--------+ | order_id | customer_id | amount | +----------+-------------+--------+101 1 180 102 2 450 103 1 220 104 3 150 +----------+-------------+--------+Which query returns the requested rows?
Show answer
Correct answer: SELECT order_id, amount FROM orders ORDER BY amount DESC LIMIT 2;
Learn more about SQLORDER BY amount DESC LIMIT 2sorts the rows from highest amount to lowest and returns only the first two rows, which are orders 102 and 103. The temptingORDER BY amount DESCis close, but it returns all four rows instead of limiting the result set.ASCwould choose the smallest amounts, andWHERE amount > 200filters by a threshold rather than returning exactly two rows. - 8.
A team wants the rank of each sale within its region, with the highest amount ranked 1 in each region. The table is:
sales +----+--------+--------+ | id | region | amount | +----+--------+--------+1 East 200 2 East 450 3 East 300 4 West 500 5 West 150 +----+--------+--------+Which query produces that ranking?
Show answer
Correct answer: SELECT region, amount, RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rnk FROM sales;
Learn more about SQL Window FunctionsRANK() OVER (PARTITION BY region ORDER BY amount DESC)restarts the ranking inside each region and sorts amounts from largest to smallest, so the top sale in East and West both get rank 1. The common mistake is to omitPARTITION BY region, which would rank the whole table as one list.RANK(amount)is not valid window-function syntax, and partitioning byamountwould group unrelated rows together. - 9.
A data analyst needs the first order date for each customer, but only for customers whose first order happened in 2024.
orders +----------+-------------+------------+ | order_id | customer_id | order_date | +----------+-------------+------------+1 10 2023-12-15 2 10 2024-01-08 3 11 2024-03-05 4 11 2024-04-01 5 12 2023-11-20 +----------+-------------+------------+Which query returns the customer IDs and their first order date, keeping only customers whose first order date is in 2024?
Show answer
Correct answer: SELECT customer_id, MIN(order_date) AS first_order_date FROM orders GROUP BY customer_id HAVING MIN(order_date) >= '2024-01-01';
Learn more about SQLThe correct query groups by customer and uses MIN(order_date) to find each customer's first order, then HAVING filters on that aggregate so only customers whose first order is in 2024 remain. The tempting WHERE version is wrong because it removes 2023 rows before the minimum is calculated, which can change the first order date for customers like 10 and hide customers like 12 entirely. HAVING is the key clause here.
Advanced · 7 questions
- 10.
A marketing analyst needs the second-highest order amount in each region. If two orders tie on amount, they should share the same rank. Which query returns the correct output?
orders +----------+--------+--------+ | order_id | region | amount | +----------+--------+--------+1 East 500 2 East 700 3 East 700 4 West 300 5 West 450 6 West 200 +----------+--------+--------+Show answer
Correct answer: SELECT region, amount FROM ( SELECT region, amount, DENSE_RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rnk FROM orders ) t WHERE rnk = 2;
Learn more about SQL Window FunctionsDENSE_RANK() assigns tied amounts the same rank and does not skip rank values, so the second-highest distinct amount in each region is returned correctly. ROW_NUMBER() would break ties arbitrarily and give only one of the 700 rows for East, which is not what the question asks. Filtering amount = 2 is unrelated, and MAX(amount) with HAVING COUNT(*) = 2 checks group size, not rank. This tests ranking semantics, not just sorting.
- 11.
A finance team wants regions whose total sales exceed the average total sales across all regions. Which query is correct?
sales +----+--------+--------+ | id | region | amount | +----+--------+--------+1 East 100 2 East 200 3 West 150 4 West 50 5 South 400 6 South 100 +----+--------+--------+Show answer
Correct answer: SELECT region, SUM(amount) AS region_total FROM sales GROUP BY region HAVING SUM(amount) > (SELECT AVG(region_total) FROM (SELECT SUM(amount) AS region_total FROM sales GROUP BY region) x);
Learn more about Common Table ExpressionsThe condition compares each region’s grouped total against the average of all grouped totals, so the query needs a nested aggregate: one subquery computes totals per region, and the outer HAVING compares each region total to their average. SUM(amount) > AVG(amount) is a different comparison inside the same group and does not compare across regions. WHERE cannot use aggregates like SUM(amount). This is a common analyst-style grouped subquery pattern.
- 12.
A retail analyst wants the top order amount per customer, but only from orders placed in February 2024. Which query returns the correct rows?
orders +----------+-------------+------------+--------+ | order_id | customer_id | order_date | amount | +----------+-------------+------------+--------+1 10 2024-01-15 80 2 10 2024-02-05 120 3 10 2024-02-20 90 4 20 2024-02-03 50 5 20 2024-03-01 200 6 30 2024-02-28 75 +----------+-------------+------------+--------+Show answer
Correct answer: SELECT customer_id, amount FROM ( SELECT customer_id, amount, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn FROM orders WHERE order_date >= '2024-02-01' AND order_date < '2024-03-01' ) t WHERE rn = 1;
Learn more about SQL Window FunctionsThe date filter must be applied before the window function ranks rows, otherwise March orders can affect the per-customer result. The correct query filters February rows first, then uses ROW_NUMBER() to pick the highest amount within each customer. The second option tries to filter on order_date after the subquery but no longer has that column in scope. BETWEEN with an end date of '2024-03-01' is also a common trap because it includes only the boundary date, not the whole month in the intended way.
- 13.
A sales analyst needs to compute the share of each order within its customer's total spend. The same order date can appear more than once.
orders +----------+-------------+--------+------------+ | order_id | customer_id | amount | order_date | +----------+-------------+--------+------------+101 1 80 2024-01-05 102 1 20 2024-01-05 103 2 50 2024-01-06 104 2 50 2024-01-07 +----------+-------------+--------+------------+Which query returns each order with a
pct_of_customer_totalcolumn equal toamount / SUM(amount) OVER (PARTITION BY customer_id)?Show answer
Correct answer: SELECT order_id, customer_id, amount, amount * 1.0 / SUM(amount) OVER (PARTITION BY customer_id) AS pct_of_customer_total FROM orders;
Learn more about SQL Window FunctionsThe correct query uses a window function with
PARTITION BY customer_id, so each row is divided by the total spend of that customer while preserving the original rows. The temptingGROUP BYversion changes the result set to one row per group and cannot return each order with its own percentage.PARTITION BY order_idwould make each denominator just one order, andORDER BY customer_idcreates a running total across customers instead of a per-customer total. - 14.
A data analyst wants the latest order date for each customer, but only for customers whose latest order was in March 2024.
orders +----------+-------------+------------+ | order_id | customer_id | order_date | +----------+-------------+------------+11 1 2024-03-02 12 1 2024-01-10 13 2 2024-03-15 14 2 2024-02-20 15 3 2024-02-28 +----------+-------------+------------+Which query returns one row per customer with the latest order date, limited to customers whose latest date falls in March?
Show answer
Correct answer: SELECT customer_id, MAX(order_date) AS latest_order_date FROM orders GROUP BY customer_id HAVING MAX(order_date) LIKE '2024-03%';
Learn more about GROUP BY and HAVINGThe correct query groups by customer, computes each customer’s latest date with
MAX(order_date), and filters the grouped result inHAVINGso only customers whose maximum date is in March remain. The temptingWHERE order_date LIKE '2024-03%'filters rows before aggregation, which can incorrectly miss a customer whose latest order is in March but who also has earlier non-March orders.WHERE MAX(...)is invalid, and grouping byorder_datechanges the grain. - 15.
A subscription team stores daily activity in this table:
activity +------------+---------+-------+ | event_date | user_id | event | +------------+---------+-------+2024-04-01 1 view 2024-04-01 1 click 2024-04-01 2 view 2024-04-02 1 view 2024-04-02 3 click +------------+---------+-------+Which query returns one row per
event_datewith the number of distinct users active that day, sorted by date?Show answer
Correct answer: SELECT event_date, COUNT(DISTINCT user_id) AS active_users FROM activity GROUP BY event_date ORDER BY event_date;
Learn more about SQLCOUNT(DISTINCT user_id)counts each user once per date, which is what the question asks for.COUNT(user_id)would count activity rows, so user 1 on 2024-04-01 would be counted twice because they have two events. TheWHERE event = 'view'option changes the metric by excluding clicks, andCOUNT(DISTINCT event)counts event types, not users. - 16.
An analyst needs each order, plus the customer's total order amount repeated on every row, using one clean query block for reuse in a later filter. Which query is correct?
orders +----------+-------------+--------+ | order_id | customer_id | amount | +----------+-------------+--------+1 10 50 2 10 70 3 11 40 4 11 60 5 12 90 +----------+-------------+--------+Show answer
Correct answer: WITH totals AS (SELECT customer_id, SUM(amount) AS total_amount FROM orders GROUP BY customer_id) SELECT o.order_id, o.customer_id, o.amount, t.total_amount FROM orders o JOIN totals t ON o.customer_id = t.customer_id;
Learn more about Common Table ExpressionsThe correct solution uses a CTE to pre-aggregate by customer_id, then joins that summary back to the detail rows. This produces one customer total repeated for each order. The second option is missing GROUP BY in the CTE, so it is invalid or collapses to a single row depending on the database. The GROUP BY order-level query returns each order's own amount, not the customer total. The last option aggregates by order_id, which is the wrong grain and cannot produce customer totals.
Common mistakes in SQL interview tests
The biggest mistakes candidates make are reading the question too quickly, assuming a join will return one row per record, and forgetting that aggregates and window functions behave differently. Another common issue is mishandling NULLs, especially in filters and joins, where = NULL does not work the way many people expect. Candidates also lose points by grouping at the wrong level or using HAVING when the condition should be in WHERE.
For advanced questions, the most important skill is tracing the row set before and after each step. A CTE can make the logic easier to follow, but it does not change the result by itself; it just structures the query. Window functions are especially easy to misuse when you need rankings, running totals, or deduplication across partitions.
In the days before an interview or take-home test, review the basics: filtering, joins, grouping, and NULL behavior. Then practice reading sample data and predicting the exact output of a query. If you can explain why each row appears, disappears, or gets duplicated, you are much better prepared for the kind of SQL questions data analysts are usually asked.
Want more questions like these?
Practice timed questions at your level, track your skill score and find the gaps before the interview. Free.
Practice more questions