Skip to content
BytePatterns

Users Retained Month Over Month

HardSQL#cte#self-join#retention~35m

Problem

An app logs every session in activity(user_id, day), with day as a YYYY-MM-DD date, and a user can have many sessions in a month. A user is active in a month if they have at least one session in it, and retained in a month if they are active in it and were also active in the calendar month right before it. For every month that has activity, return month as YYYY-MM, the number of active users and the number of retained users, sorted by month.

Examples

Input:  activity = [(1, "2026-01-05"), (1, "2026-01-20"), (2, "2026-01-09"), (1, "2026-02-02"), (3, "2026-02-14"), (2, "2026-03-01"), (3, "2026-03-30")]
Output: [("2026-01", 2, 0), ("2026-02", 2, 1), ("2026-03", 2, 1)]
Why:    user 1 is retained in February and user 3 in March; user 2 skipped February, so March does not count them
Input:  activity = [(1, "2026-04-10"), (1, "2026-06-10")]
Output: [("2026-04", 1, 0), ("2026-06", 1, 0)]
Why:    May has no activity, so it gets no row, and June's previous month is May, not April
Input:  activity = [(7, "2025-12-31"), (7, "2026-01-01"), (8, "2026-01-15")]
Output: [("2025-12", 1, 0), ("2026-01", 2, 1)]
Why:    edge case, the month before January is December of the previous year

Hints

0 / 3

Stuck on the idea rather than the code? CTEs and Recursion covers it.