Phase 8 · SQL interview problems, from easy to hard · Topic 8.10

Each customer's first order

Must knowInterview problemMedium

For each customer who has ordered, show their first order — its id, date and amount — and how many days after signing up they placed it.

Not started

Step 1 Solve it yourself first

Tables you will use

orders customers

What it really tests

Picking exactly one per group (the first event per user) with , and joining back for extra . Used for activation, onboarding and funnel questions.

The pattern: First (or latest) row per group

"First purchase", "most recent login", "latest status", "earliest", one per user.

Picture it

flowchart LR
  O["Priya's orders by date<br/>101 · 102 · 103 · 105 · 109"] --> R["ROW_NUMBER<br/>1 · 2 · 3 · 4 · 5"]
  R --> K["keep 1:<br/>order 101 on 2024-01-06"]
Number each customer's orders from oldest to newest, then keep number 1. The same pattern with gives the latest order.

The approach, step by step

  1. Number each customer's orders by date: ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id).
  2. Add order_id as a in case two orders share a date.
  3. Keep row number 1 in an outer .
  4. Join customers for the name and sign-up date, and work out the days between.

Your turn: write it before you look

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.
Show the solution

Stuck for more than ten minutes? Open it, read it, close it, and write it again from memory.

Solution: ROW_NUMBER = 1

sql
WITH numbered AS (
  SELECT customer_id, order_id, order_date, amount,
         ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS rn
  FROM orders
)
SELECT c.name, n.order_id, n.order_date, n.amount,
       julianday(n.order_date) - julianday(c.signup_date) AS days_to_first_order
FROM numbered n
JOIN customers c ON c.customer_id = n.customer_id
WHERE n.rn = 1
ORDER BY c.customer_id;

4 rows

nameorder_idorder_dateamountdays_to_first_order
Priya1012024-01-061204
Tom1042024-01-212001
Lena1062024-02-12NULL13
Zoe1102024-03-16336

Lena's first order is the cancelled one with no amount. Ask whether "first order" should mean first delivered order.

Another way: date, then match it

sql
SELECT c.name, o.order_id, o.order_date, o.amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_date = (
  SELECT MIN(o2.order_date)
  FROM orders o2
  WHERE o2.customer_id = o.customer_id
)
ORDER BY c.customer_id;

4 rows

nameorder_idorder_dateamount
Priya1012024-01-06120
Tom1042024-01-21200
Lena1062024-02-12NULL
Zoe1102024-03-1633

Returns two rows for a customer with two orders on their first day. ROW_NUMBER with a tiebreaker guarantees one.

Edge cases to say out loud

  • Two orders on the same first day: the tiebreaker (order_id) decides; say which one you keep.
  • Cancelled first orders: confirm whether they count.
  • Customers with no orders: not in the result — from customers if they should appear.

Common mistakes

  • GROUP BY customer_id with MIN(order_date) and amount in — the amount isn't tied to the first order.
  • Forgetting , so only one order in the whole gets number 1.
  • No tiebreaker in ROW_NUMBER, so the answer can change between runs.

In other databases

  • has a shortcut: SELECT DISTINCT ON (customer_id) * FROM orders ORDER BY customer_id, order_date, order_id.
  • Days between: PostgreSQL order_date - signup_date for columns; DATEDIFF(order_date, signup_date).

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.

Step 3 Practise

Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.

  1. ExplainSay what it does and when to use it, in plain words, with no notes.

    Explain why ROW_NUMBER needs a tiebreaker here, with an example.

    Show answer

    ROW_NUMBER must give exactly one row number 1 per customer. If two orders share the earliest date, ordering by date alone leaves the tie unbroken, and the may pick either one, and pick differently on another run or after an changes. The answer looks fine but isn't repeatable.

    Example: if Priya had placed orders 101 and 111 both on 2024-01-06, ORDER BY order_date could return either as her first order. Adding order_id as a second sort key (ORDER BY order_date, order_id) makes 101 win every time.

  2. Writefrom a blank page, then run it

    For each customer, show their latest order and its status.

    Show answer

    Number each customer's orders newest first, with order_id as the tiebreaker, and keep number 1. Zoe's latest (and only) order is still pending.

    sql
    WITH numbered AS (
      SELECT customer_id, order_id, order_date, status,
             ROW_NUMBER() OVER (
               PARTITION BY customer_id
               ORDER BY order_date DESC, order_id DESC
             ) AS rn
      FROM orders
    )
    SELECT c.name, n.order_id, n.order_date, n.status
    FROM numbered n
    JOIN customers c ON c.customer_id = n.customer_id
    WHERE n.rn = 1
    ORDER BY c.customer_id;

    4 rows

    nameorder_idorder_datestatus
    Priya1092024-03-10delivered
    Tom1082024-03-01delivered
    Lena1072024-02-20delivered
    Zoe1102024-03-16pending
  3. Predictsay it before you run it

    What would the solution return for Lena if the only numbered delivered orders?

    Show answer

    Lena's first delivered order is 107 (20 February, £60), not the cancelled 106, so her days to first order become 21 instead of 13 (she signed up on 30 January).

    And Zoe disappears: her only order is pending, so she has no delivered orders to number.

    sql
    WITH numbered AS (
      SELECT customer_id, order_id, order_date, amount,
             ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS rn
      FROM orders
      WHERE status = 'delivered'
    )
    SELECT c.name, n.order_id, n.order_date, n.amount,
           julianday(n.order_date) - julianday(c.signup_date) AS days_to_first_order
    FROM numbered n
    JOIN customers c ON c.customer_id = n.customer_id
    WHERE n.rn = 1
    ORDER BY c.customer_id;

    3 rows

    nameorder_idorder_dateamountdays_to_first_order
    Priya1012024-01-061204
    Tom1042024-01-212001
    Lena1072024-02-206021
  4. Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.

    Change the solution so customers with no orders appear with .

    Show answer

    Start from customers and LEFT JOIN the numbered orders. The n.rn = 1 test must go in the : in the , it would be NULL for customers with no orders and would drop them again. Omar and the second Tom now appear with NULLs.

    sql
    WITH numbered AS (
      SELECT customer_id, order_id, order_date, amount,
             ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS rn
      FROM orders
    )
    SELECT c.name, n.order_id, n.order_date, n.amount,
           julianday(n.order_date) - julianday(c.signup_date) AS days_to_first_order
    FROM customers c
    LEFT JOIN numbered n ON n.customer_id = c.customer_id AND n.rn = 1
    ORDER BY c.customer_id;

    6 rows

    nameorder_idorder_dateamountdays_to_first_order
    Priya1012024-01-061204
    Tom1042024-01-212001
    Lena1062024-02-12NULL13
    OmarNULLNULLNULLNULL
    Zoe1102024-03-16336
    TomNULLNULLNULLNULL

Practice box: write and run your answers here

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud.

For first-per-group I number rows with ROW_NUMBER partitioned by customer and ordered by date with the order id as a tiebreaker, keep row number one in an outer query, and customers for the sign-up date to compute days to first order. I'd confirm whether cancelled orders count, and mention that a MIN-date join can return duplicates on ties while ROW_NUMBER always returns exactly one.

Follow-up questions

How would you get each customer's latest order instead?

Order the ROW_NUMBER by order_date DESC, order_id DESC and keep row 1.

How would you find each customer's first delivered order?

Add WHERE status = 'delivered' inside the CTE, before numbering.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.

  • Explain Say what it does and when to use it, in plain words, with no notes.
  • Write Write the query from a blank page, then run it to check.
  • Predict Read a query and say which rows it returns before you run it.
  • Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.