Months in Motion

A medium SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
medium
Seniority
L3
Asked in
Whole Foods

Problem

Show how many unique buyers and how many transactions each calendar month drew, with the month numbered 1 for January through 12 for December and every year in transactions pooled into those same twelve months. The analyst studying this seasonality only wants purchases of at least $50 counted.

Engagement, counted one month at a time.

Tables

transactions

Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT

transactions: 10 of 200 rows
transaction_iduser_idproduct_idquantitytotal_amounttransaction_date
10671971001223.462026-02-02
11342941050336.932026-03-03
12013911099450.42026-04-04
12684881148563.872026-05-05
13355851197002026-06-06
14026821246290.812026-07-07
146977912953104.282026-08-08
153687613444117.752026-09-09
160397313935-452026-01-10
1670107014421144.692026-02-11

Expected output

First 5 of 12 rows
monthunique_userstotal_transactions
11019
21020
31019
1046
1146

Worked solution and explanation

What this problem is really about

Underneath the seasonality framing, this is one row per calendar month with two counts at two different cardinalities hanging off it. How many people bought is COUNT(DISTINCT user_id); how many purchases happened is COUNT(*). Everyone gets the grouping roughly right. What separates candidates is refusing to treat those two numbers as one. On this seed 15 buyers make 12 to 14 purchases each, so a row count reports 19 January buyers where only 10 people bought. The quieter failure is the grain: group on the year-month string and January splits into one row per year, so the twelve-month shape never appears. Let the $50 floor slip and every month also counts the cheap, voided and refunded sales.


Building it

Step 1: Filter to qualifying purchases

Keep only rows with total_amount >= 50, in WHERE, so the floor applies before any counting. The >= keeps a purchase landing exactly on $50. This matters on the seed: a real slice of rows sits under $50, including a $0 void and a -$45 refund, and leaving the filter out changes every month's counts.

Step 2: Group on the bare month

Pull the month out of the date with strftime('%m', transaction_date) and group on that, so each row folds into one of twelve buckets regardless of its year. In the SELECT, wrap it in CAST(... AS INTEGER) so the label comes back as 1 through 12 rather than the text '01' through '12'.

Step 3: Count people and purchases separately

Inside each bucket, COUNT(DISTINCT user_id) gives unique buyers and COUNT(*) gives total transactions. They diverge whenever a buyer purchases more than once in that month, which is why both columns exist.


The solution

Filtered multi-aggregate by month
SELECT CAST(strftime('%m', transaction_date) AS INTEGER) AS month,
       COUNT(DISTINCT user_id) AS unique_users,
       COUNT(*) AS total_transactions
FROM transactions
WHERE total_amount >= 50
GROUP BY strftime('%m', transaction_date)

Common follow-up questions