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

5 November 2025 10 min read

Last updated: 8 October 2026

Share:

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.

python
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""")
text
-- 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 530

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

  1. Restate the question as something measurable: which period, which customers, which definition of "active".
  2. Profile the data for duplicates, nulls, cancellations and partial periods.
  3. Build in steps with CTEs, and check row counts after each step.
  4. Sanity-check the answer against something you know, such as the number of customers or a monthly total.
  5. 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?

Questions framed as business problems, such as finding lapsed customers, explaining a drop in revenue or measuring retention, where you choose the tables, definitions and SQL yourself and are judged on both the query and your reasoning.

How do I prepare for scenario-based SQL questions?

Practise on a real dataset with real problems, such as duplicates, missing values, cancellations and partial periods, and practise explaining each step and its assumptions out loud.

Should I clean the data before answering a scenario question?

Profile it first and state your cleaning rules. The interviewer may accept them, change them, or have planted the problem to see whether you notice.

Which SQL features come up most in scenario questions?

GROUP BY with aggregates, joins and anti-joins with NOT EXISTS, window functions such as ROW_NUMBER and LAG, CTEs to build the answer in steps, and date functions for periods and cohorts.

Practising SQL on real business data is part of the SQL Server course.

Sources

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.

Explore course