sqladvanced50 minDuckDB SQL
Consecutive Login Streak Detector (Gaps and Islands)
Detect consecutive daily user workout streaks and breaks using the classic SQL gaps-and-islands difference technique.
The incident
- Company
- HabitPulse Health
- Engineer
- Neha Sharma, Product Data Scientist
- Trigger
- A 10-day workout streak promotion awarded reward points incorrectly because missed workout days were not breaking streaks.
- Stakes
- Over 15 lakh rupees in unearned loyalty gift vouchers distributed due to faulty streak detection logic.
On a Wednesday morning, Neha Sharma was summoned to an emergency product meeting at HabitPulse Health, a fitness platform with over one million active mobile subscribers. The product team had recently launched a high-profile gamification campaign: users who logged workouts for 5 consecutive days earned 500 loyalty coins redeemable for retail merchandise. Over 15 lakh rupees in gift vouchers had already been claimed in the first two weeks. The promotion was designed to build daily workout habits, but faulty tracking threatened to deplete marketing resources prematurely.
However, customer support noticed suspicious patterns: users who worked out on Monday and Tuesday, skipped Wednesday, and logged workouts on Thursday and Friday were being awarded 5-day streak badges. The legacy backend code simply counted total workout days within the past week, completely ignoring non-consecutive missed days. Unearned vouchers were rapidly draining the promotional marketing budget. Support tickets revealed that users quickly discovered the bug, deliberately skipping workouts while continuing to accumulate unearned loyalty points.
Neha took ownership of resolving the flaw by implementing mathematically rigorous gap-and-island streak detection in SQL. The user_logins table in DuckDB recorded login_id, user_id, and login_date. To calculate genuine streaks, Neha needed to group consecutive daily dates into continuous islands while treating any missed calendar day as a streak boundary. Neha recognized that standard relational aggregations could not detect continuous sequences without a mathematical grouping technique.
Neha employed the classic SQL difference technique. When a sequence of dates advances by exactly 1 day and a ROW_NUMBER() counter also advances by 1, subtracting the row number from the date produces an identical anchor date for all consecutive days in that streak. The moment a gap of one or more days occurs, the difference shifts to a new anchor date, creating a new island. This elegant invariant transformed the non-trivial challenge of streak detection into a straightforward grouping and aggregation query.
Within four hours, Neha validated the gaps-and-islands query against test activity data. The query demonstrated that user 101 did not have a continuous 5-day streak; instead, they completed a 3-day streak (March 1 to March 3) followed by a 1-day gap, and then started a new 2-day streak (March 5 to March 6). The query cleanly separated the two activity periods, establishing an indisputable factual baseline for award distribution.
Product engineering integrated Neha's SQL streak algorithm into the real-time rewards engine. Streak rewards were locked strictly to continuous workout islands, immediately halting improper voucher distributions and recovering 15 lakh rupees in projected marketing losses. The gamification platform now rewards genuine consistency, restoring customer trust and safeguarding marketing capital. By embedding this deterministic algorithm into daily engagement reports, product managers established transparent streak criteria that motivated genuine user activity across all mobile applications.
What you will learn
- Understand the mathematical principles of the SQL gaps-and-islands problem
- Generate sequential rank integers using ROW_NUMBER() partitioned by user and ordered by date
- Calculate constant island grouping keys by subtracting integer row numbers from calendar dates
- Aggregate start dates, end dates, and streak lengths for user engagement gamification
What you need first
Gaps and Islands PrincipleSQL window functions
Subtracting an incrementing integer row number from consecutive calendar dates yields a constant anchor date across contiguous sequences.
SELECT DATE '2024-03-02' - 2 * INTERVAL '1 day' AS anchor_date;anchor_date ----------- 2024-02-29
Attempting to track streaks by comparing LAG(date) alone without grouping into island identifiers, which struggles with multi-day streaks.
Streak Duration AggregationSQL advanced aggregations
Grouping by user and island key allows computing streak duration via COUNT(*) or MAX(login_date) - MIN(login_date) + 1.
SELECT COUNT(*) AS streak_length FROM (VALUES (1), (2), (3)) t;streak_length ------------- 3
Failing to deduplicate multiple logins on the same calendar day before applying row numbers, which breaks the 1-to-1 increment alignment.
Architecture and data flow
The data
User daily login ledger recording dates of platform engagement.
user_logins: User daily login timestamp log
| login_id | user_id | login_date |
|---|---|---|
| 1 | 101 | 2024-03-01 |
| 2 | 101 | 2024-03-02 |
| 3 | 101 | 2024-03-03 |
| 4 | 101 | 2024-03-05 |
| 5 | 101 | 2024-03-06 |
CREATE OR REPLACE TABLE user_logins (
login_id INT,
user_id INT,
login_date DATE
);
INSERT INTO user_logins VALUES
(1, 101, DATE '2024-03-01'),
(2, 101, DATE '2024-03-02'),
(3, 101, DATE '2024-03-03'),
(4, 101, DATE '2024-03-05'),
(5, 101, DATE '2024-03-06'),
(6, 102, DATE '2024-03-01'),
(7, 102, DATE '2024-03-02');Step by step
Read each step, run its code, then check your output against the one shown.
Inspect chronological user login activity
We begin by querying the raw user_logins table ordered by user_id and login_date. User 101 logged in on March 1, 2, and 3, skipped March 4, and returned on March 5 and 6. User 102 logged in on March 1 and 2. In gamification analytics, inspecting the baseline activity confirms that users have distinct login dates and that date gaps are clearly visible before applying window calculations. Examining raw login logs verifies that users engage across multiple days.
SELECT login_id, user_id, login_date FROM user_logins ORDER BY user_id, login_date;login_id | user_id | login_date ---------+---------+----------- 1 | 101 | 2024-03-01 2 | 101 | 2024-03-02 3 | 101 | 2024-03-03 4 | 101 | 2024-03-05 5 | 101 | 2024-03-06 6 | 102 | 2024-03-01 7 | 102 | 2024-03-02
Common mistakeAssuming login_date is always unique per user without performing an initial DISTINCT deduplication step.
How you will knowYou will see 7 login records ordered cleanly by user and login date.
Attempt naive date difference span
Wrong turn first: this step shows the mistake before the fix
Here is the naive approach: computing streak length by simply subtracting MIN(login_date) from MAX(login_date) per user. For user 101, March 6 minus March 1 plus 1 yields 6 days, completely ignoring the fact that the user was inactive on March 4. This naive query creates phantom streaks across inactive intervals, leading to erroneous reward allocations. Calculating the span between minimum and maximum dates completely ignores skipped days, resulting in inflated streak counts that drain promotional loyalty budgets prematurely. In production user analytics, relying on crude min-max span calculations misrepresents true customer engagement and compromises the integrity of gamification leaderboards.
SELECT user_id, MIN(login_date) AS first_login, MAX(login_date) AS last_login, (MAX(login_date) - MIN(login_date)) + 1 AS naive_streak_span FROM user_logins GROUP BY user_id ORDER BY user_id;user_id | first_login | last_login | naive_streak_span --------+-------------+------------+------------------ 101 | 2024-03-01 | 2024-03-06 | 6 102 | 2024-03-01 | 2024-03-02 | 2
Common mistakeCalculating total date span between min and max dates instead of detecting contiguous unbroken intervals.
How you will knowNotice that naive date span reports 6 days for user 101, masking the missing login on March 4.
Assign chronological row numbers per user
We apply the ROW_NUMBER() window function partitioned by user_id and ordered by login_date. This generates a continuous integer sequence (1, 2, 3, 4, 5) for user 101. On consecutive days, both the calendar date and the row number advance by exactly 1. When a calendar day is skipped (March 4), the date advances by 2 while the row number advances by only 1, creating a mathematical divergence. This divergence between calendar days and sequential row numbers is the fundamental mechanism that allows SQL queries to detect gaps in temporal activity.
SELECT user_id, login_date, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ) AS seq_num FROM user_logins ORDER BY user_id, login_date;user_id | login_date | seq_num --------+------------+-------- 101 | 2024-03-01 | 1 101 | 2024-03-02 | 2 101 | 2024-03-03 | 3 101 | 2024-03-05 | 4 101 | 2024-03-06 | 5 102 | 2024-03-01 | 1 102 | 2024-03-02 | 2
Common mistakeForgetting PARTITION BY user_id, which causes row numbers to increment across multiple users and destroys user isolation.
How you will knowOutput shows seq_num incrementing from 1 to 5 for user 101 and 1 to 2 for user 102.
Derive constant island grouping anchor date
We compute the island grouping key by subtracting seq_num days from login_date. For user 101: March 1 minus 1 day = 2024-02-29; March 2 minus 2 days = 2024-02-29; March 3 minus 3 days = 2024-02-29. All three consecutive days share the exact same anchor date. On March 5 (seq_num 4), March 5 minus 4 days = 2024-03-01, creating a brand new island group for the second streak. Subtracting row numbers from calendar dates produces identical anchor dates across contiguous sequences, neatly partitioning discontinuous activity into discrete island identifiers.
SELECT user_id, login_date, login_date - CAST(ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ) AS INT) * INTERVAL '1 day' AS island_group FROM user_logins ORDER BY user_id, login_date;user_id | login_date | island_group --------+------------+-------------------- 101 | 2024-03-01 | 2024-02-29 00:00:00 101 | 2024-03-02 | 2024-02-29 00:00:00 101 | 2024-03-03 | 2024-02-29 00:00:00 101 | 2024-03-05 | 2024-03-01 00:00:00 101 | 2024-03-06 | 2024-03-01 00:00:00 102 | 2024-03-01 | 2024-02-29 00:00:00 102 | 2024-03-02 | 2024-02-29 00:00:00
Common mistakeUsing integer subtraction directly without multiplying by INTERVAL '1 day' in engines that require explicit date intervals.
How you will knowMarch 1-3 share island_group 2024-02-29; March 5-6 share island_group 2024-03-01.
Synthesize streak summary with start, end, and duration
In the final query, we group by user_id and island_group to summarize every discrete streak. We compute streak_start with MIN(login_date), streak_end with MAX(login_date), and streak_length using COUNT(*). The result correctly identifies 3 distinct streaks: user 101 has a 3-day streak and a 2-day streak, while user 102 has a 2-day streak. The query delivers the exact data shape required by product gamification engines. Aggregating by island group computes exact streak durations.
WITH island_calc AS ( SELECT user_id, login_date, login_date - CAST(ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ) AS INT) * INTERVAL '1 day' AS island_group FROM user_logins ) SELECT user_id, MIN(login_date) AS streak_start, MAX(login_date) AS streak_end, CAST(COUNT(*) AS INT) AS streak_length FROM island_calc GROUP BY user_id, island_group ORDER BY user_id, streak_start;user_id | streak_start | streak_end | streak_length --------+--------------+------------+-------------- 101 | 2024-03-01 | 2024-03-03 | 3 101 | 2024-03-05 | 2024-03-06 | 2 102 | 2024-03-01 | 2024-03-02 | 2
Common mistakeGrouping only by user_id without island_group, which merges separate streaks back into a single total day count.
How you will knowThe output provides 3 discrete streaks with exact start dates, end dates, and streak lengths.
Build it yourself
Write your solution below, run it in your browser, and pass every check.
What the result means
Interpretation
The gaps-and-islands analysis accurately disproves the assumption that user 101 maintained a continuous 5-day streak. By subtracting sequential row numbers from login dates, the model isolated two separate activity islands: a 3-day streak ending March 3, and a new 2-day streak starting March 5. This ensures reward criteria are strictly enforced and prevents premature voucher disbursement.
The message you would send
Hi Gamification Product Team, the SQL gaps-and-islands streak detection logic is complete. The model reliably breaks streaks whenever a calendar day is missed, preventing improper reward distributions. Integration with the rewards engine will safeguard our promotional budget.
Put it on your CV
Resume or LinkedIn bullet
Implemented the classic SQL gaps-and-islands algorithm to detect consecutive daily activity streaks and breaks in DuckDB.README for GitHub
# Consecutive Login Streak Detector (Gaps and Islands)
An advanced SQL pipeline solving the classic gaps-and-islands problem to detect unbroken daily activity streaks.
## Features
- Chronological row numbering partitioned by user entity
- Constant island grouping derivation via date arithmetic (date - row_num * interval)
- Exact unbroken streak length, start date, and end date aggregation
- Scalable pattern for gamification, retention streaks, and sensor anomaly detectionExtend the query to find each user all-time maximum streak length and rank users on a global leaderboard.