Scenario-Based SQL Interview Questions
Scenario questions give you a business problem instead of a SQL topic, and the data usually hides a trap. These eight scenarios are solved on a year of real online retail orders, with real traps: duplicate lines, a partial final month, missing customer IDs and a huge order cancelled twelve minutes later.
GloryTecks Team
Training & Placement Faculty
Last updated: 8 October 2026
Scenario questions describe a business problem and leave the SQL to you: "which customers stopped buying?", "how is revenue trending?", "what do people buy together?". They test two things at once: whether you can turn the question into a query, and whether you notice what the data is hiding. These scenario-based SQL interview questions are solved on a real dataset, because real data has the traps that made-up practice tables leave out.
The data is the UCI Online Retail set: every order line from a UK online gift retailer between 1 December 2010 and 9 December 2011, with invoice numbers beginning with C marking cancellations (UCI: Online Retail). DuckDB reads the Excel file directly. The invoice column mixes numbers with C-codes, so the sheet is read as text with all_varchar and converted (DuckDB: Excel extension); the dates arrive as Excel serial numbers, days since 30 December 1899, and are converted too.
import duckdb
db = duckdb.connect()
db.execute("INSTALL excel; LOAD excel")
db.execute("""CREATE TABLE raw AS SELECT * FROM read_xlsx('Online Retail.xlsx', all_varchar = true)""") # UCI Online Retail
db.execute("""CREATE TABLE lines AS SELECT
InvoiceNo AS invoice, StockCode AS product, TRIM(Description) AS description,
CAST(Quantity AS INTEGER) AS qty, ROUND(CAST(UnitPrice AS DOUBLE), 2) AS price,
CAST(CAST(CustomerID AS DOUBLE) AS INTEGER) AS customer, Country AS country,
TIMESTAMP '1899-12-30' + to_seconds(CAST(ROUND(CAST(InvoiceDate AS DOUBLE) * 86400) AS BIGINT)) AS ts -- Excel serial date
FROM raw""")
def ask(title, sql):
print(f"-- {title}")
for row in db.execute(sql).fetchall():
print(" ", *row)
ask("S1 the data first: rows, date range, exact duplicate lines, cancellations, lines without a customer", """
SELECT COUNT(*), MIN(ts)::DATE, MAX(ts)::DATE,
COUNT(*) - (SELECT COUNT(*) FROM (SELECT DISTINCT * FROM lines)) AS duplicate_lines,
COUNT(*) FILTER (WHERE invoice LIKE 'C%') AS cancellation_lines,
COUNT(*) FILTER (WHERE customer IS NULL) AS no_customer
FROM lines""")
db.execute("""CREATE TABLE sales AS SELECT DISTINCT * FROM lines
WHERE customer IS NOT NULL AND invoice NOT LIKE 'C%' AND qty > 0 AND price > 0""")
ask("S2 monthly revenue: does the last month really collapse?", """
SELECT strftime(ts, '%Y-%m') AS month, ROUND(SUM(qty * price)) AS revenue,
COUNT(DISTINCT ts::DATE) AS trading_days, MAX(ts)::DATE AS last_day
FROM sales WHERE ts >= '2011-09-01' GROUP BY month ORDER BY month""")
ask("S3 customers who bought in the first half of 2011 but not the second", """
SELECT COUNT(DISTINCT customer) FROM sales a
WHERE ts >= '2011-01-01' AND ts < '2011-07-01'
AND NOT EXISTS (SELECT 1 FROM sales b WHERE b.customer = a.customer AND b.ts >= '2011-07-01')""")
ask("S4 each customer's latest order (one row per customer)", """
WITH orders AS (SELECT customer, invoice, MIN(ts) AS ts, ROUND(SUM(qty * price), 2) AS value
FROM sales GROUP BY customer, invoice),
ranked AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY customer ORDER BY ts DESC, invoice DESC) AS n FROM orders)
SELECT COUNT(*) AS customers, COUNT(*) FILTER (WHERE ts < '2011-09-01') AS last_order_before_september
FROM ranked WHERE n = 1""")
ask("S5 cohort: customers whose first order was in December 2010, still buying N months later", """
WITH firsts AS (SELECT customer, MIN(ts) AS first_ts FROM sales GROUP BY customer),
cohort AS (SELECT customer FROM firsts WHERE first_ts >= '2010-12-01' AND first_ts < '2011-01-01'),
active AS (SELECT DISTINCT s.customer, date_diff('month', DATE '2010-12-01', s.ts::DATE) AS months_later
FROM sales s JOIN cohort USING (customer))
SELECT months_later, COUNT(*) AS customers,
ROUND(100.0 * COUNT(*) / (SELECT COUNT(*) FROM cohort), 1) AS pct_of_cohort
FROM active WHERE months_later IN (0, 1, 3, 6, 11) GROUP BY months_later ORDER BY months_later""")
ask("S6 repeat customers: share with more than one order", """
SELECT COUNT(*) AS customers, ROUND(100.0 * AVG(CASE WHEN orders > 1 THEN 1 ELSE 0 END), 1) AS pct_repeat
FROM (SELECT customer, COUNT(DISTINCT invoice) AS orders FROM sales GROUP BY customer)""")
ask("S7 median and mean order value (one outlier is enough to separate them)", """
WITH orders AS (SELECT invoice, SUM(qty * price) AS value FROM sales GROUP BY invoice)
SELECT ROUND(MEDIAN(value), 2) AS median, ROUND(AVG(value), 2) AS mean, ROUND(MAX(value), 2) AS largest FROM orders""")
ask(" the largest order, and any cancellation of the same product by the same customer", """
WITH biggest AS (SELECT invoice, customer, product, qty, ts FROM sales
WHERE invoice = (SELECT invoice FROM sales GROUP BY invoice ORDER BY SUM(qty * price) DESC LIMIT 1))
SELECT b.invoice, b.ts, b.qty, c.invoice AS cancelled_by, c.ts AS cancelled_at, c.qty
FROM biggest b LEFT JOIN lines c
ON c.customer = b.customer AND c.product = b.product AND c.invoice LIKE 'C%' AND c.qty = -b.qty""")
ask("S8 products most often bought together (pairs in the same order)", """
WITH items AS (SELECT DISTINCT invoice, description FROM sales)
SELECT a.description, b.description, COUNT(*) AS orders
FROM items a JOIN items b ON a.invoice = b.invoice AND a.description < b.description
GROUP BY ALL ORDER BY orders DESC, a.description LIMIT 3""")-- S1 the data first: rows, date range, exact duplicate lines, cancellations, lines without a customer
541909 2010-12-01 2011-12-09 5268 9288 135080
-- S2 monthly revenue: does the last month really collapse?
2011-09 950690.0 26 2011-09-30
2011-10 1035642.0 26 2011-10-31
2011-11 1156206.0 26 2011-11-30
2011-12 517190.0 8 2011-12-09
-- S3 customers who bought in the first half of 2011 but not the second
853
-- S4 each customer's latest order (one row per customer)
4338 1365
-- S5 cohort: customers whose first order was in December 2010, still buying N months later
0 885 100.0
1 324 36.6
3 340 38.4
6 321 36.3
11 445 50.3
-- S6 repeat customers: share with more than one order
4338 65.6
-- S7 median and mean order value (one outlier is enough to separate them)
302.58 479.56 168469.6
-- the largest order, and any cancellation of the same product by the same customer
581483 2011-12-09 09:15:00 80995 C581484 2011-12-09 09:27:00 -80995
-- S8 products most often bought together (pairs in the same order)
JUMBO BAG PINK POLKADOT JUMBO BAG RED RETROSPOT 546
GREEN REGENCY TEACUP AND SAUCER ROSES REGENCY TEACUP AND SAUCER 541
ALARM CLOCK BAKELIKE GREEN ALARM CLOCK BAKELIKE RED 530S1. Before any scenario: what is wrong with this data?
Interviewers notice when a candidate profiles the data before answering, and with this file it matters. Of 541,909 lines, 5,268 are exact duplicates of another line, 9,288 belong to cancellations, and 135,080 have no customer ID, even though the dataset page lists no missing values. Each of those would distort a customer-level answer. The query then builds a sales table with duplicates, cancellations, lines without a customer and non-positive quantities or prices removed, and says so. State your cleaning rules out loud; the interviewer may want different ones.
S2. "Revenue collapsed in December 2011. What happened?"
Nothing, as far as the data shows: the data stops on 9 December. December has 8 trading days against 26 in each of the three months before, so its £517,190 is a partial month, not a fall. Comparing a partial period with full ones is one of the most common analysis mistakes, and scenario questions often plant it. Report revenue per trading day, or leave the partial month out of the trend and say why.
S3. "Which customers bought in the first half of 2011 but not the second?"
Customers active between January and June with no order from July onwards: NOT EXISTS against the later period gives 853. Use NOT EXISTS rather than NOT IN so that a NULL cannot empty the result. Then ask the follow-up yourself: is six months the right window for a business whose customers include wholesalers who order a few times a year?
S4. "Give me each customer's most recent order."
Build orders from lines first, then rank each customer's orders by date with ROW_NUMBER and keep number 1. Ordering by date alone could tie when one customer placed two orders in the same minute, so the invoice number breaks the tie. Of 4,338 customers, 1,365 had not ordered since before September 2011, which is useful context for S3.
S5. "How well do we keep new customers?"
A cohort question. Take customers whose first order was in December 2010, then count how many ordered in later months. Of 885 such customers, 36.6% ordered again in January, 36.3% in June, and 50.3% in November 2011. The rise in November is seasonal, since gifts sell before Christmas, which is worth saying instead of reading it as improving loyalty. Two cautions: December 2010 is the first month of the data, so some of these "new" customers may have bought before the data begins; and a monthly retention curve depends on the cohort's season.
S6. "What share of customers come back?"
65.6% of the 4,338 customers placed more than one order. Count distinct invoices per customer, not lines: one order of twenty products is still one order.
S7. "What is a typical order worth?"
The median order was £302.58 and the mean £479.56. When the two differ that much, a few very large orders are pulling the mean up, so the median, the middle value, is the better "typical" figure (DuckDB: aggregate functions). Then look at the largest order: £168,469.60 for 80,995 units of one product, invoice 581483, at 09:15 on 9 December 2011. A cancellation, C581484, for exactly 80,995 units of the same product by the same customer followed twelve minutes later.
This is the trap inside the trap. Removing cancellation lines, as S1 did, does not remove the orders they cancel. The order is still in sales, and it lifts the December figure in S2 as well. A correct revenue calculation nets cancellations against their orders, or works from net quantities per customer and product. Finding this before the interviewer points it out is exactly what scenario rounds reward.
S8. "Which products are bought together?"
Self-join the order lines on the invoice, keeping each pair once with a.description < b.description. The top pairs were two jumbo bag designs, 546 orders, then two regency teacup designs and two alarm clock colours. The < condition stops a product pairing with itself and stops each pair being counted in both orders. On large data, warn that a self-join grows with the square of basket size, and filter to frequent products first.
How to work through a scenario in the room
- Restate the question as something measurable: which period, which customers, which definition of "active".
- Profile the data for duplicates, nulls, cancellations and partial periods.
- Build in steps with CTEs, and check row counts after each step.
- Sanity-check the answer against something you know, such as the number of customers or a monthly total.
- Say what you would check next. In S7 that was the cancelled order.
For the SQL behind these steps, see window function interview questions for S4 and S5, and SQL joins interview questions for S3 and S8. The day-to-day queries analysts write are collected in SQL for data analysts, and the full topic list is in SQL interview questions: top 50 with answers.
What are scenario-based SQL interview questions?
How do I prepare for scenario-based SQL questions?
Should I clean the data before answering a scenario question?
Which SQL features come up most in scenario questions?
Practising SQL on real business data is part of the SQL Server course.
Sources
- Online Retail dataset (UCI Machine Learning Repository) — UCI Machine Learning Repository
- Excel Extension (DuckDB documentation) — DuckDB
- Aggregate Functions (DuckDB documentation) — DuckDB
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Interview Questions?
Join GloryTecks’ expert-led SQL Server training in Hyderabad with real-time projects and 100% placement support.
Related articles
SQL Interview Questions: Top 50 with Answers
Fifty SQL interview questions with answers checked on PostgreSQL, DuckDB and SQLite against one small practice schema: evaluation order, NULL logic, joins and fan-out, window functions, dates, and performance, including where the three engines disagree.
SQL Joins Interview Questions Explained
Join questions in interviews are rarely about syntax. They test whether you can predict how many rows come back. These nine questions, each answered with a query on three million real New York taxi trips, cover anti-joins, ON versus WHERE, NOT IN with NULLs, fan-out, self-joins, calendars and full outer joins.
Window Functions SQL Interview Questions
Interviewers use window functions to see whether you understand how SQL evaluates a query, not just the syntax. These nine questions, each answered with a query run on two years of real bike rentals, cover ranking ties, top N per group, running totals, the default frame trap, gaps and islands, and filtering.