Advanced Case 48 of 50

Case 048: The Customer Who Was Due

customersorders

"Averages and standard deviations are nice,"

the manager says, "but could you turn that into something actionable? Tell me who's actually overdue for their next order."

The data Alex is looking at

customers
idnamecountrysignup_date
1SamGermany2026-01-02
2NinaCanada2026-01-05
3DavidGermany2026-01-10
4MayaJapan2026-01-15
5DanielCanada2026-01-20
… 5 more rows
orders
idcustomer_idorder_date
10112026-01-05
10222026-01-08
10332026-01-12
10412026-01-20
10542026-02-01
… 23 more rows
TASK

Help Alex answer the manager:

“As of 2026-06-25, find every customer whose time since their last order already exceeds their own typical (average) gap between orders — flag them as overdue, showing their last order date and average gap.”
SQL editor Ctrl+Enter to run
Write a query above and hit Run to see what comes back.
No hints requested yet — click "Get a hint" when you're stuck.
Alex's final query — this is for reference. Copy it into your own thinking, not into the editor above.
WITH gaps AS ( SELECT customer_id, order_date, DATEDIFF( order_date, LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) ) AS gap_days FROM orders ), customer_rhythm AS ( SELECT customer_id, AVG(gap_days) AS avg_gap_days, MAX(order_date) AS last_order_date FROM gaps WHERE gap_days IS NOT NULL GROUP BY customer_id HAVING COUNT(gap_days) >= 2 ) SELECT customers.name, ROUND(customer_rhythm.avg_gap_days, 1) AS avg_gap_days, customer_rhythm.last_order_date, CAST( DATEDIFF('2026-06-25', customer_rhythm.last_order_date) - customer_rhythm.avg_gap_days AS SIGNED ) AS days_overdue FROM customer_rhythm JOIN customers ON customers.id = customer_rhythm.customer_id WHERE DATEDIFF('2026-06-25', customer_rhythm.last_order_date) > customer_rhythm.avg_gap_days ORDER BY days_overdue DESC;
What that query returns
Case Debrief Predictive flagging (LAG + AVG + date math)
  • Predictive flags don't need machine learning — comparing current behavior against a customer's own historical average, entirely in SQL, gets you surprisingly far.
  • Quick tip: CAST(... AS SIGNED) truncates toward zero rather than rounding — 4.5 becomes 4, not 5. ROUND(..., 0) would round to the nearest whole day instead, which would overstate how overdue someone is (rounding 4.5 up to 5 claims a full 5th day has passed when it hasn't). Truncating only counts full, completed days past the average gap, which is what "overdue" should mean.

Case closed.

Ready for the next one?

Next: Case 049 — Filling the Gaps →