By Matthew Rathbone · October 27, 2026

Data analyst SQL interviews are less about trivia and more about turning a business question into a query: "What was revenue last month?", "Who are our repeat customers?", "How fast are we growing?" Expect a few concept questions, then a live exercise with joins, GROUP BY and usually at least one window function.

This page walks through 19 questions in that style. Each has a short explanation, a query and its real output. If you want the general list (keys, normalization, DELETE vs TRUNCATE), see our 30 SQL interview questions and answers.

Jump to: set up the data · the questions · what analyst interviewers look for · FAQ

Set up the practice database

All queries use a small online shop with customers, products, orders and order_items. Paste this into a free SQLite client such as Beekeeper Studio, then try each question yourself before reading the answer. Queries use SQLite syntax; notes show how PostgreSQL, MySQL and SQL Server differ.

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, country TEXT, signup_date TEXT);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, order_date TEXT, status TEXT);
CREATE TABLE order_items (order_id INTEGER, product_id INTEGER, quantity INTEGER);

INSERT INTO customers VALUES
  (1, 'Ana Silva', 'Brazil', '2025-01-15'),
  (2, 'Ben Carter', 'USA', '2025-02-03'),
  (3, 'Chloe Dubois', 'France', '2025-02-20'),
  (4, 'Dev Patel', 'India', '2025-03-11'),
  (5, 'Emma Jones', 'USA', '2025-04-02'),
  (6, 'Felix Wagner', 'Germany', '2025-05-18'),
  (7, 'Grace Kim', 'USA', '2025-06-07'),
  (8, 'Hiro Tanaka', 'Japan', '2025-06-30');

INSERT INTO products VALUES
  (1, 'Laptop', 'Electronics', 899.00),
  (2, 'Headphones', 'Electronics', 129.50),
  (3, 'Desk Lamp', 'Home', 34.99),
  (4, 'Notebook', 'Stationery', 4.25),
  (5, 'Pen Set', 'Stationery', 12.00),
  (6, 'Office Chair', 'Home', 189.00),
  (7, 'Webcam', 'Electronics', 59.90),
  (8, 'Backpack', 'Accessories', 45.00);

INSERT INTO orders VALUES
  (1, 1, '2025-07-01', 'shipped'),
  (2, 2, '2025-07-03', 'shipped'),
  (3, 2, '2025-07-15', 'cancelled'),
  (4, 3, '2025-07-20', 'shipped'),
  (5, 5, '2025-08-02', 'shipped'),
  (6, 1, '2025-08-09', 'pending'),
  (7, 4, '2025-08-12', 'shipped'),
  (8, 5, '2025-08-21', 'shipped'),
  (9, 7, '2025-09-04', 'shipped'),
  (10, 2, '2025-09-10', 'shipped'),
  (11, 3, '2025-09-18', 'pending'),
  (12, 7, '2025-09-25', 'cancelled');

INSERT INTO order_items VALUES
  (1, 1, 1), (1, 4, 3),
  (2, 2, 1), (2, 7, 1),
  (3, 6, 1),
  (4, 3, 2), (4, 5, 2),
  (5, 8, 1), (5, 4, 5),
  (6, 2, 2),
  (7, 1, 1), (7, 2, 1),
  (8, 5, 3),
  (9, 6, 1), (9, 3, 1),
  (10, 4, 10), (10, 8, 2),
  (11, 7, 1),
  (12, 1, 1);

Data analyst SQL interview questions

Questions 1 to 6 are business metrics, 7 to 12 cover customers and segments, and 13 to 19 are the window function and data-cleaning questions that separate strong candidates.

1. What is the difference between a metric, a dimension and a grain?

Analysts talk like this all the time, so interviewers check you do too. A metric is a number you measure (revenue, order count). A dimension is how you slice it (country, month, category). The grain is what one row in a table represents: here order_items has one row per product per order, while orders has one row per order. Mixing grains is the most common cause of wrong totals, because joining orders to order_items repeats each order once per item.

2. What is the total revenue from shipped orders?

Revenue lives at the order item level (quantity * price), and 'revenue' usually excludes cancelled and pending orders. State that assumption out loud.

SELECT ROUND(SUM(oi.quantity * p.price), 2) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'shipped';

Result (1 row):

revenue
2682.37

3. What is revenue by month?

Group by a date expression. In SQLite use strftime('%Y-%m', date); PostgreSQL uses DATE_TRUNC('month', date) and MySQL uses DATE_FORMAT(date, '%Y-%m').

SELECT strftime('%Y-%m', o.order_date) AS month,
       ROUND(SUM(oi.quantity * p.price), 2) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'shipped'
GROUP BY month
ORDER BY month;

Result (3 rows):

monthrevenue
2025-071195.13
2025-081130.75
2025-09356.49

4. How do you calculate month-over-month growth?

Put the monthly totals in a CTE, then use LAG() to pull in the previous month's value. The first month has no previous month, so its growth is NULL. Read more in LAG and LEAD.

WITH monthly AS (
  SELECT strftime('%Y-%m', o.order_date) AS month,
         SUM(oi.quantity * p.price) AS revenue
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  JOIN products p ON p.id = oi.product_id
  WHERE o.status = 'shipped'
  GROUP BY month
)
SELECT month,
       ROUND(revenue, 2) AS revenue,
       ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
             / LAG(revenue) OVER (ORDER BY month), 1) AS growth_pct
FROM monthly;

Result (3 rows):

monthrevenuegrowth_pct
2025-071195.13NULL
2025-081130.75-5.4
2025-09356.49-68.5

5. How do you calculate a running total?

A window function with SUM() OVER (ORDER BY ...) keeps every row and adds the cumulative sum.

WITH monthly AS (
  SELECT strftime('%Y-%m', o.order_date) AS month,
         SUM(oi.quantity * p.price) AS revenue
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  JOIN products p ON p.id = oi.product_id
  WHERE o.status = 'shipped'
  GROUP BY month
)
SELECT month,
       ROUND(revenue, 2) AS revenue,
       ROUND(SUM(revenue) OVER (ORDER BY month), 2) AS running_total
FROM monthly;

Result (3 rows):

monthrevenuerunning_total
2025-071195.131195.13
2025-081130.752325.88
2025-09356.492682.37

6. What percentage of revenue does each product category contribute?

Divide each group's total by the grand total. A window function over the grouped result gives you the grand total without a second query.

SELECT p.category,
       ROUND(SUM(oi.quantity * p.price), 2) AS revenue,
       ROUND(100.0 * SUM(oi.quantity * p.price) / SUM(SUM(oi.quantity * p.price)) OVER (), 1) AS pct_of_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'shipped'
GROUP BY p.category
ORDER BY revenue DESC;

Result (4 rows):

categoryrevenuepct_of_total
Electronics2116.978.9
Home293.9711.0
Stationery136.55.1
Accessories135.05.0

Yes, a window function can wrap an aggregate: GROUP BY runs first, then SUM() OVER () adds up the grouped rows.

7. Who are the top 3 customers by revenue?

The classic. Join, group, sort, limit. Mention what you would do about ties (see the next question).

SELECT c.name,
       ROUND(SUM(oi.quantity * p.price), 2) AS revenue
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'shipped'
GROUP BY c.id, c.name
ORDER BY revenue DESC
LIMIT 3;

Result (3 rows):

namerevenue
Dev Patel1028.5
Ana Silva911.75
Ben Carter321.9

8. What is the best-selling product in each category?

'Top N per group' needs a window function. ROW_NUMBER() with PARTITION BY restarts the numbering for each category, then you keep row 1. Use RANK() instead if ties should both be kept. See DENSE_RANK and RANK.

WITH product_sales AS (
  SELECT p.category, p.name, SUM(oi.quantity) AS units
  FROM order_items oi
  JOIN products p ON p.id = oi.product_id
  JOIN orders o ON o.id = oi.order_id
  WHERE o.status = 'shipped'
  GROUP BY p.id, p.category, p.name
),
ranked AS (
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY category ORDER BY units DESC) AS rn
  FROM product_sales
)
SELECT category, name, units
FROM ranked
WHERE rn = 1
ORDER BY category;

Result (4 rows):

categorynameunits
AccessoriesBackpack3
ElectronicsLaptop2
HomeDesk Lamp3
StationeryNotebook18

9. How many customers have placed more than one order (repeat customers)?

Aggregate to one row per customer first, then count how many pass the bar. Counting distinct orders protects you from join fan-out.

SELECT COUNT(*) AS repeat_customers
FROM (
  SELECT customer_id
  FROM orders
  GROUP BY customer_id
  HAVING COUNT(*) > 1
) AS repeaters;

Result (1 row):

repeat_customers
5

10. Which customers have never placed an order?

Anti-join: LEFT JOIN and keep the rows where the right side is NULL. NOT EXISTS works too. See LEFT JOIN.

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

Result (2 rows):

idname
6Felix Wagner
8Hiro Tanaka

11. What is the order cancellation rate?

Conditional aggregation: a boolean expression is 1 or 0, so AVG of it is the share. Multiply by 100.0 (not 100) to avoid integer division.

SELECT COUNT(*) AS total_orders,
       SUM(status = 'cancelled') AS cancelled,
       ROUND(100.0 * AVG(status = 'cancelled'), 1) AS cancellation_pct
FROM orders;

Result (1 row):

total_orderscancelledcancellation_pct
12216.7

In PostgreSQL and SQL Server use SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END), which works everywhere. See CASE WHEN.

12. How do you count orders per status in separate columns (pivot)?

SQL has no universal PIVOT, so analysts use CASE WHEN inside aggregates. See also how to pivot in SQL.

SELECT strftime('%Y-%m', order_date) AS month,
       SUM(CASE WHEN status = 'shipped'   THEN 1 ELSE 0 END) AS shipped,
       SUM(CASE WHEN status = 'pending'   THEN 1 ELSE 0 END) AS pending,
       SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled
FROM orders
GROUP BY month
ORDER BY month;

Result (3 rows):

monthshippedpendingcancelled
2025-07301
2025-08310
2025-09211

13. What is the average order value (AOV)?

Average of order totals, not of line items. Build order totals in a subquery, then average them. Averaging line items directly is the classic wrong answer.

SELECT ROUND(AVG(order_total), 2) AS avg_order_value
FROM (
  SELECT o.id, SUM(oi.quantity * p.price) AS order_total
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  JOIN products p ON p.id = oi.product_id
  WHERE o.status = 'shipped'
  GROUP BY o.id
) AS order_totals;

Result (1 row):

avg_order_value
335.3

14. How do you find duplicate rows?

Group by the columns that should be unique and keep groups with a count above one. The sample data has no duplicates, so here we look for customers who share a country.

SELECT country, COUNT(*) AS customers
FROM customers
GROUP BY country
HAVING COUNT(*) > 1;

Result (1 row):

countrycustomers
USA3

To delete duplicates, number the rows with ROW_NUMBER() OVER (PARTITION BY ... ORDER BY id) and delete everything where the number is greater than 1. Always SELECT first to check what you are about to remove.

15. What is each customer's first order date and days to their second order?

Number each customer's orders with ROW_NUMBER(), then compare dates. julianday() is SQLite's date difference; PostgreSQL subtracts dates directly and SQL Server uses DATEDIFF. Related: comparing dates.

WITH numbered AS (
  SELECT customer_id, order_date,
         ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) AS n
  FROM orders
)
SELECT a.customer_id,
       a.order_date AS first_order,
       b.order_date AS second_order,
       CAST(julianday(b.order_date) - julianday(a.order_date) AS INTEGER) AS days_between
FROM numbered a
JOIN numbered b ON b.customer_id = a.customer_id AND b.n = 2
WHERE a.n = 1
ORDER BY a.customer_id;

Result (5 rows):

customer_idfirst_ordersecond_orderdays_between
12025-07-012025-08-0939
22025-07-032025-07-1512
32025-07-202025-09-1860
52025-08-022025-08-2119
72025-09-042025-09-2521

16. How do you calculate a 2-order moving average?

Add a window frame: ROWS BETWEEN 1 PRECEDING AND CURRENT ROW averages the current and previous row. Read what a moving average is.

WITH monthly AS (
  SELECT strftime('%Y-%m', o.order_date) AS month,
         SUM(oi.quantity * p.price) AS revenue
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  JOIN products p ON p.id = oi.product_id
  WHERE o.status = 'shipped'
  GROUP BY month
)
SELECT month,
       ROUND(revenue, 2) AS revenue,
       ROUND(AVG(revenue) OVER (ORDER BY month
             ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 2) AS moving_avg
FROM monthly;

Result (3 rows):

monthrevenuemoving_avg
2025-071195.131195.13
2025-081130.751162.94
2025-09356.49743.62

17. How do you rank customers into quartiles?

NTILE(4) splits ordered rows into four equal buckets. Analysts use it for segmentation like 'top 25% of customers'.

WITH spend AS (
  SELECT c.name, SUM(oi.quantity * p.price) AS revenue
  FROM customers c
  JOIN orders o ON o.customer_id = c.id
  JOIN order_items oi ON oi.order_id = o.id
  JOIN products p ON p.id = oi.product_id
  WHERE o.status = 'shipped'
  GROUP BY c.id, c.name
)
SELECT name,
       ROUND(revenue, 2) AS revenue,
       NTILE(4) OVER (ORDER BY revenue DESC) AS quartile
FROM spend
ORDER BY revenue DESC;

Result (6 rows):

namerevenuequartile
Dev Patel1028.51
Ana Silva911.751
Ben Carter321.92
Grace Kim223.992
Emma Jones102.253
Chloe Dubois93.984

18. How do you handle NULLs in a report?

Use COALESCE to replace NULL with a default. Here a LEFT JOIN leaves customers without orders with a NULL count, and COUNT(o.id) correctly ignores them. See the COALESCE lesson.

SELECT c.name,
       COUNT(o.id) AS orders,
       COALESCE(MAX(o.order_date), 'never') AS last_order
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY orders DESC, c.name;

Result (8 rows):

nameorderslast_order
Ben Carter32025-09-10
Ana Silva22025-08-09
Chloe Dubois22025-09-18
Emma Jones22025-08-21
Grace Kim22025-09-25
Dev Patel12025-08-12
Felix Wagner0never
Hiro Tanaka0never

19. Which customers have more orders than the average customer?

Compare each group to an overall figure with a scalar subquery. The subquery returns one number, the average orders per customer who ordered.

SELECT customer_id, COUNT(*) AS orders
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > (SELECT COUNT(*) * 1.0 / COUNT(DISTINCT customer_id) FROM orders);

Result (1 row):

customer_idorders
23

What data analyst interviewers look for

  • Definitions before code. Ask what counts as revenue, an active customer or a repeat order. Cancelled orders are the usual trap.
  • Grain awareness. Say what one row represents after each join. Joining orders to order items repeats order-level values, which inflates sums and averages.
  • Sanity checks. Compare your total against a simple query. If monthly revenue does not add up to total revenue, something is wrong.
  • Readable SQL. Use CTEs with clear names instead of deeply nested subqueries, and alias every column you output.
  • Explain the result. Finish with one sentence on what the numbers mean. Analysts are hired to communicate, not just query.

Where to go next

FAQ

What SQL do data analysts need for interviews?

Joins, GROUP BY with aggregates, CASE WHEN, subqueries and CTEs, date functions, and window functions (ROW_NUMBER, RANK, LAG, running totals). Performance tuning matters less than for engineers.

How hard are SQL interviews for data analysts?

Most are medium difficulty: a business question that needs two or three joins and one aggregate or window function. The hard part is usually interpreting the question, not the syntax.

Which SQL dialect should I practice for a data analyst interview?

Ask the recruiter. If they do not say, practice PostgreSQL-style standard SQL and learn the date function differences for MySQL and SQL Server.

How long does it take to prepare?

If you know basic SELECT and joins, one to two weeks of daily practice covers aggregates, CTEs and window functions. Starting from zero, plan for a month. See is SQL easy to learn?

Where can I practice for free?

Run the queries above in any SQLite client, or use SQL Easy's free interactive lessons, which run in your browser with instant feedback.