✏️ Explanatory Question
Level: Hard — Notoriously tricky; the "difference of two sequences" trick is the elegant senior-level solution.
user_logins (user 1)| login_date | Note |
|---|---|
| 2026-01-01 | Island 1 (3-day streak) |
| 2026-01-02 | |
| 2026-01-03 | |
| 2026-01-06 | Island 2 (2-day streak) — gap on 4th & 5th |
| 2026-01-07 | |
| 2026-01-10 | Island 3 (1-day streak) |
(date − ROW_NUMBER) stays constant within a streak. As dates increase by 1 and the row number also increases by 1, their difference is unchanged — until a gap breaks it. That constant difference becomes the group key for each island.
| login_date | ROW_NUMBER (rn) | date − rn days | Group |
|---|---|---|---|
| 2026-01-01 | 1 | 2025-12-31 | A |
| 2026-01-02 | 2 | 2025-12-31 | A |
| 2026-01-03 | 3 | 2025-12-31 | A |
| 2026-01-06 | 4 | 2026-01-02 | B |
| 2026-01-07 | 5 | 2026-01-02 | B |
| 2026-01-10 | 6 | 2026-01-04 | C |
Rows with the same "date − rn" value belong to the same consecutive streak.
SELECT
MIN(login_date) AS streak_start,
MAX(login_date) AS streak_end,
COUNT(*) AS streak_length
FROM (
SELECT
login_date,
-- subtract the row-number (in days) to get a constant group key
DATE_SUB(
login_date,
INTERVAL ROW_NUMBER() OVER (ORDER BY login_date) DAY
) AS grp
FROM user_logins
WHERE user_id = 1
) t
GROUP BY grp
ORDER BY streak_start;
| streak_start | streak_end | streak_length |
|---|---|---|
| 2026-01-01 | 2026-01-03 | 3 |
| 2026-01-06 | 2026-01-07 | 2 |
| 2026-01-10 | 2026-01-10 | 1 |
-- Use LEAD() to find where the next login jumps more than 1 day
SELECT
DATE_ADD(login_date, INTERVAL 1 DAY) AS gap_start,
DATE_SUB(next_login, INTERVAL 1 DAY) AS gap_end
FROM (
SELECT login_date,
LEAD(login_date) OVER (ORDER BY login_date) AS next_login
FROM user_logins
WHERE user_id = 1
) t
WHERE DATEDIFF(next_login, login_date) > 1; -- a gap exists
-- Gap: 2026-01-04 to 2026-01-05, and 2026-01-08 to 2026-01-09
value − ROW_NUMBER() with no date arithmetic. The same "difference of two monotonic sequences" idea applies.
PARTITION BY user_id in the ROW_NUMBER, then GROUP BY user_id, grp, and finally take MAX(streak_length) per user — the gaps-and-islands pattern scales cleanly with partitioning.