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. 1.

    A data analyst is checking a small customer table.

    customers
    +----+--------+--------+
    | id | name   | city   |
    +----+--------+--------+
    1AvaMiami
    2BenDallas
    3CaraMiami
    +----+--------+--------+

    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;

    This 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.

    Learn more about SQL
  2. 2.

    A report needs order IDs with amounts above 100.

    orders
    +---------+-----------+--------+
    | order_id| customer  | amount |
    +---------+-----------+--------+
    101Ava80
    102Ben120
    103Cara150
    +---------+-----------+--------+

    Which query returns the two qualifying order IDs?

    Show answer

    Correct answer: SELECT order_id FROM orders WHERE amount > 100;

    The 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.

    Learn more about SQL
  3. 3.

    A team wants one row per customer with their total spend.

    orders
    +----------+-------------+--------+
    | order_id | customer_id | amount |
    +----------+-------------+--------+
    11040
    21060
    32030
    +----------+-------------+--------+

    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;

    The 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.

    Learn more about GROUP BY and HAVING
  4. 4.

    An analyst wants a running total by month for one store.

    sales
    +----+-------+--------+
    | id | month | amount |
    +----+-------+--------+
    1Jan50
    2Feb30
    3Mar20
    +----+-------+--------+

    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;

    The 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.

    Learn more about SQL Window Functions
  5. 5.

    A reporting query needs one row per department after filtering departments with at least two employees.

    staff
    +----+----------+--------+
    | id | dept     | title  |
    +----+----------+--------+
    1SalesRep
    2SalesLead
    3HRAnalyst
    4ITAdmin
    +----+----------+--------+

    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;

    This 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.

    Learn more about GROUP BY and HAVING
  6. 6.

    A reporting team has these tables:

    customers
    +----+--------+
    | id | name   |
    +----+--------+
    1Ava
    2Ben
    3Cara
    +----+--------+
    
    orders
    +----------+-------------+--------+
    | order_id | customer_id | amount |
    +----------+-------------+--------+
    10140
    11125
    12260
    +----------+-------------+--------+

    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;

    LEFT 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.

    Learn more about SQL Joins

Intermediate · 3 questions

  1. 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 |
    +----------+-------------+--------+
    1011180
    1022450
    1031220
    1043150
    +----------+-------------+--------+

    Which query returns the requested rows?

    Show answer

    Correct answer: SELECT order_id, amount FROM orders ORDER BY amount DESC LIMIT 2;

    ORDER BY amount DESC LIMIT 2 sorts the rows from highest amount to lowest and returns only the first two rows, which are orders 102 and 103. The tempting ORDER BY amount DESC is close, but it returns all four rows instead of limiting the result set. ASC would choose the smallest amounts, and WHERE amount > 200 filters by a threshold rather than returning exactly two rows.

    Learn more about SQL
  2. 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 |
    +----+--------+--------+
    1East200
    2East450
    3East300
    4West500
    5West150
    +----+--------+--------+

    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;

    RANK() 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 omit PARTITION BY region, which would rank the whole table as one list. RANK(amount) is not valid window-function syntax, and partitioning by amount would group unrelated rows together.

    Learn more about SQL Window Functions
  3. 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 |
    +----------+-------------+------------+
    1102023-12-15
    2102024-01-08
    3112024-03-05
    4112024-04-01
    5122023-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';

    The 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.

    Learn more about SQL

Advanced · 7 questions

  1. 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 |
    +----------+--------+--------+
    1East500
    2East700
    3East700
    4West300
    5West450
    6West200
    +----------+--------+--------+
    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;

    DENSE_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.

    Learn more about SQL Window Functions
  2. 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 |
    +----+--------+--------+
    1East100
    2East200
    3West150
    4West50
    5South400
    6South100
    +----+--------+--------+
    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);

    The 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.

    Learn more about Common Table Expressions
  3. 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 |
    +----------+-------------+------------+--------+
    1102024-01-1580
    2102024-02-05120
    3102024-02-2090
    4202024-02-0350
    5202024-03-01200
    6302024-02-2875
    +----------+-------------+------------+--------+
    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;

    The 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.

    Learn more about SQL Window Functions
  4. 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 |
    +----------+-------------+--------+------------+
    1011802024-01-05
    1021202024-01-05
    1032502024-01-06
    1042502024-01-07
    +----------+-------------+--------+------------+

    Which query returns each order with a pct_of_customer_total column equal to amount / 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;

    The 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 tempting GROUP BY version changes the result set to one row per group and cannot return each order with its own percentage. PARTITION BY order_id would make each denominator just one order, and ORDER BY customer_id creates a running total across customers instead of a per-customer total.

    Learn more about SQL Window Functions
  5. 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 |
    +----------+-------------+------------+
    1112024-03-02
    1212024-01-10
    1322024-03-15
    1422024-02-20
    1532024-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%';

    The correct query groups by customer, computes each customer’s latest date with MAX(order_date), and filters the grouped result in HAVING so only customers whose maximum date is in March remain. The tempting WHERE 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 by order_date changes the grain.

    Learn more about GROUP BY and HAVING
  6. 15.

    A subscription team stores daily activity in this table:

    activity
    +------------+---------+-------+
    | event_date | user_id | event |
    +------------+---------+-------+
    2024-04-011view
    2024-04-011click
    2024-04-012view
    2024-04-021view
    2024-04-023click
    +------------+---------+-------+

    Which query returns one row per event_date with 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;

    COUNT(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. The WHERE event = 'view' option changes the metric by excluding clicks, and COUNT(DISTINCT event) counts event types, not users.

    Learn more about SQL
  7. 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 |
    +----------+-------------+--------+
    11050
    21070
    31140
    41160
    51290
    +----------+-------------+--------+
    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;

    The 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.

    Learn more about Common Table Expressions
16 questions

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