Repeat Purchase Window

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
L4
Asked in
Amazon

Problem

An analyst building a churn model needs the customers whose buying rhythm has slipped: the ones who once let an unusually long stretch open up between one purchase and the very next, yet came back rather than going silent for good. From the transactions table, find the users who at some point had 65 to 80 days between consecutive purchases, and return each qualifying user ID once, smallest first.

The retention squad is looking for repeat purchasers.

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
user_id
197
294
391
488
682

Worked solution and explanation

What this problem really is

Underneath the churn story, this is a per-customer inter-purchase gap detection. For each customer, order their purchases by date, measure how far each one sits from the purchase right before it, and keep the customers whose gap lands inside a two-sided day band. The trap is the band's shape. Every customer here re-buys within days or weeks and, somewhere across the years, lets a lapse of many months open up. So a one-sided test ('gap of at least 65 days' or 'gap of at most 80 days') lets all fifteen customers through and flags your regulars and your fully-lapsed alike. Only fencing both ends isolates the twelve whose cadence genuinely drifted into the moderate-lapse band.


Walking the solution

Step 1: Look up each customer's prior purchase date

You cannot see a gap without the neighbouring row's date. LAG(transaction_date) OVER (PARTITION BY user_id ORDER BY transaction_date) hands each purchase the date of that customer's immediately preceding purchase. Partitioning by user_id keeps the comparison inside one customer; ordering by date is what makes 'the one before' meaningful.

Step 2: Turn two dates into a gap in days

julianday() turns each TEXT date into a day number, so julianday(transaction_date) - julianday(prev_date) is the gap in whole days. Real date arithmetic, not string comparison, is what keeps month and year boundaries correct. A customer's first purchase has a NULL prev_date, so its gap is NULL and never qualifies.

Step 3: Fence both ends of the window

BETWEEN 65 AND 80 does two jobs at once. The lower bound drops the short in-season gaps every customer has, and the upper bound drops the lapses that ran longer, across seasons and years. Remove either bound and the filter collapses to every customer. Both ends carry weight, which is exactly why the band must be two-sided.

Step 4: Return each customer once, in order

A customer who slips into the band more than once would otherwise appear several times, so DISTINCT collapses them to a single row. Sorting by user_id gives the smallest-first list the prompt asks for.


The solution

Slipping-cadence customers via LAG gap
SELECT DISTINCT user_id
FROM (
  SELECT user_id,
         transaction_date,
         LAG(transaction_date) OVER (PARTITION BY user_id ORDER BY transaction_date) AS prev_date
  FROM transactions
)
WHERE julianday(transaction_date) - julianday(prev_date) BETWEEN 65 AND 80
ORDER BY user_id

Common follow-up questions