Average Fulfillment Lag

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

Domain
SQL
Difficulty
easy
Seniority
L4
Asked in
Airbnb

Problem

Find each user's average purchase age in days, counting from each date in transactions up to today, oldest averages first. We only want real sales in that number, so rows with a zero or negative amount stay out.

Ordered, then... waiting.

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 15 rows
user_idavg_days
197862.2142857142857
1167820.3333333333334
682809.6428571428571
294650.2142857142857
779647.7142857142857

Worked solution and explanation

This is a per-user average of date differences, dressed up as a customer recency metric. Everyone writes the GROUP BY user_id and the AVG. What separates candidates is deciding which rows are purchases at all, and what 'today' means, before typing. The seed hides a voided sale (total_amount of 0.0 for user 585) and a refund (-45.0 for user 973). Leave them in and those two users' averages absorb dates that were never purchases, so the list is wrong exactly for the customers with returns.

Build it in three decisions

Step 1: Keep only real sales

A refund or a void is a row in transactions but not a purchase. WHERE total_amount > 0 drops both before anything is averaged. Once the bad rows are inside a user's mean, no clause after the aggregate can take them back out.

Step 2: Turn each date into a day count

julianday(date('now')) - julianday(transaction_date) is the age of one purchase in days. date('now') is today with no time of day, and transaction_date is a plain date, so every per-row gap is a whole number of days.

Step 3: Average per user, oldest first

GROUP BY user_id with AVG(...) gives one row per buyer, keeping the fractional average. ORDER BY avg_days DESC puts the buyers with the oldest purchase history at the top, which is how the list is read.

Average purchase age per user
SELECT user_id, AVG(julianday(date('now')) - julianday(transaction_date)) AS avg_days FROM transactions WHERE total_amount > 0 GROUP BY user_id ORDER BY avg_days DESC
Every row averaged
No WHERE: user 973's -45.0 refund and user 585's voided 0.0 sale each contribute a date, so their avg_days drift toward those dates.
Real sales only
WHERE total_amount > 0: each average is built only from purchases. The other thirteen users come out identical either way, which is why the bug survives a quick eyeball check.

Follow-up questions

Common follow-up questions