SQL Gaps and Islands: Finding Consecutive Streaks
"Find users who logged in three days in a row" is a classic hard interview question. The rows that form an unbroken run are called an island, and the breaks between them are gaps. One clever trick with ROW_NUMBER turns each island into a group you can count.
Examples run in MySQL 8 and PostgreSQL · Updated
In short
- Number each user's rows in date order with
ROW_NUMBER. - Day number minus row number stays constant within a streak.
- Group by that constant to get one row per streak.
- Remove duplicate dates first, or the numbering breaks.
The trick
Within a run of consecutive days, the date and the row number both go up by 1 per row, so date − row_number does not change. After a gap the date jumps further than the row number, so the difference changes. That difference is a label for each island.
To keep the examples identical in MySQL and PostgreSQL, the logins all fall in one month and use EXTRACT(DAY FROM login_date) as the day number.
With real dates
Across months, subtract the row number as days instead: DATE_SUB(login_date, INTERVAL rn DAY) in MySQL, or login_date - rn::int in PostgreSQL. The grouping step stays the same.
The example tables
Every example on this page uses these tables. They are small on purpose, so you can check each result by eye.
logins
| user_id | login_date |
|---|---|
| 1 | 2025-03-01 |
| 1 | 2025-03-02 |
| 1 | 2025-03-03 |
| 1 | 2025-03-05 |
| 1 | 2025-03-06 |
| 2 | 2025-03-01 |
| 2 | 2025-03-03 |
| 2 | 2025-03-04 |
| 2 | 2025-03-05 |
| 2 | 2025-03-06 |
| 3 | 2025-03-10 |
Examples
Label each island
For user 1, March 1–3 get island 0 and March 5–6 get island 1, because the gap on the 4th shifts the difference.
SELECT user_id, login_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS rn,
EXTRACT(DAY FROM login_date) - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS island
FROM logins
ORDER BY user_id, login_date;| user_id | login_date | rn | island |
|---|---|---|---|
| 1 | 2025-03-01 | 1 | 0 |
| 1 | 2025-03-02 | 2 | 0 |
| 1 | 2025-03-03 | 3 | 0 |
| 1 | 2025-03-05 | 4 | 1 |
| 1 | 2025-03-06 | 5 | 1 |
| 2 | 2025-03-01 | 1 | 0 |
| 2 | 2025-03-03 | 2 | 1 |
| 2 | 2025-03-04 | 3 | 1 |
| 2 | 2025-03-05 | 4 | 1 |
| 2 | 2025-03-06 | 5 | 1 |
| 3 | 2025-03-10 | 1 | 9 |
One row per streak
Group by user and island to get each streak's first day, last day and length.
WITH numbered AS (
SELECT user_id, login_date,
EXTRACT(DAY FROM login_date) - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS island
FROM logins
)
SELECT user_id, MIN(login_date) AS streak_start, MAX(login_date) AS streak_end, COUNT(*) AS days
FROM numbered
GROUP BY user_id, island
ORDER BY user_id, streak_start;| user_id | streak_start | streak_end | days |
|---|---|---|---|
| 1 | 2025-03-01 | 2025-03-03 | 3 |
| 1 | 2025-03-05 | 2025-03-06 | 2 |
| 2 | 2025-03-01 | 2025-03-01 | 1 |
| 2 | 2025-03-03 | 2025-03-06 | 4 |
| 3 | 2025-03-10 | 2025-03-10 | 1 |
Users with a 3-day streak
The longest streak per user, keeping only users with at least three days in a row.
WITH numbered AS (
SELECT user_id, login_date,
EXTRACT(DAY FROM login_date) - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS island
FROM logins
),
streaks AS (
SELECT user_id, COUNT(*) AS days
FROM numbered
GROUP BY user_id, island
)
SELECT user_id, MAX(days) AS longest_streak
FROM streaks
GROUP BY user_id
HAVING MAX(days) >= 3
ORDER BY user_id;| user_id | longest_streak |
|---|---|
| 1 | 3 |
| 2 | 4 |
Common mistakes
- Duplicate dates. Two logins on one day get different row numbers but the same date, which splits the streak. Start from
SELECT DISTINCT user_id, login_date. - Negative numbers in MySQL.
ROW_NUMBER()is unsigned in MySQL, sosmall_number - ROW_NUMBER()can raise an "out of range" error. Subtract in an order that stays positive, or use the date form. - Checking only neighbours. Comparing each row with
LAGfinds where a streak starts but not its length. Combine it with a running count, or use the ROW_NUMBER trick.
Practise Gaps and Islands
Interview questions that use this concept, easiest first. Each one has the step-by-step visualizer and answer checking.
User Consecutive 3-Day Login Streak
Detect users who logged into Google services on at least three consecutive calendar days.
Longest Login Streaks
Find users who logged in on at least three consecutive days and report their best streak.
Shows at #1 for Three Weeks
Find TV shows that held the weekly #1 spot for at least three consecutive weeks.
Stadium Crowd Streaks
Find days that belong to a run of three or more consecutive busy days.
Job Status Periods
Collapse daily job results into continuous succeeded and failed periods.
Want a full set? Work through the Dates & Time study plan.
Questions people ask
- Can I solve it with LAG instead?
- Yes. Flag rows where the gap to the previous date is more than one day, take a running SUM of the flags as the streak id, then group by it. Both approaches are common in interviews.
- Does this work for numbers, not dates?
- Yes. The same trick finds runs of consecutive ids or seat numbers: value minus row number is constant within each run.