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.

SQLMini example
SELECT DATE '2024-03-02' - 2 * INTERVAL '1 day' AS anchor_date;
anchor_date
-----------
2024-02-29
Watch out

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.

SQLMini example
SELECT COUNT(*) AS streak_length FROM (VALUES (1), (2), (3)) t;
streak_length
-------------
3
Watch out

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

Faulty streak calculation promotion reward problem diagram
Faulty streak calculation promotion reward problem diagram. Naive day counting awards loyalty points across non-consecutive missed workout days.
Gaps and islands SQL data flow diagram
Gaps and islands SQL data flow diagram. Subtracting row numbers from calendar dates generates constant island group anchors.
Streak island summary output shape diagram
Streak island summary output shape diagram. Summarized streak table with start and end dates and exact unbroken streak lengths.

The data

User daily login ledger recording dates of platform engagement.

user_logins: User daily login timestamp log

login_iduser_idlogin_date
11012024-03-01
21012024-03-02
31012024-03-03
41012024-03-05
51012024-03-06
SQLSeed data, preloaded in your session
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.

  1. 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.

    SQLStep 1
    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 mistake

    Assuming login_date is always unique per user without performing an initial DISTINCT deduplication step.

    How you will know

    You will see 7 login records ordered cleanly by user and login date.

  2. 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.

    SQLStep 2
    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 mistake

    Calculating total date span between min and max dates instead of detecting contiguous unbroken intervals.

    How you will know

    Notice that naive date span reports 6 days for user 101, masking the missing login on March 4.

  3. 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.

    SQLStep 3
    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 mistake

    Forgetting PARTITION BY user_id, which causes row numbers to increment across multiple users and destroys user isolation.

    How you will know

    Output shows seq_num incrementing from 1 to 5 for user 101 and 1 to 2 for user 102.

  4. 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.

    SQLStep 4
    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 mistake

    Using integer subtraction directly without multiplying by INTERVAL '1 day' in engines that require explicit date intervals.

    How you will know

    March 1-3 share island_group 2024-02-29; March 5-6 share island_group 2024-03-01.

  5. 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.

    SQLStep 5
    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 mistake

    Grouping only by user_id without island_group, which merges separate streaks back into a single total day count.

    How you will know

    The 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.

DuckDB SQL (In-Browser)Ready to run
Loading editor…
Press Run Code to evaluate
Query executes without SQL errors
Result contains all required streak summary columns
Exactly 3 distinct streak islands returned
First streak is user 101
User 101 first streak starts 2024-03-01
User 101 first streak ends 2024-03-03
User 101 first streak length is 3
User 101 second streak length is 2
Third streak is user 102
User 102 streak length is 2

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

OutputCopy this line
Implemented the classic SQL gaps-and-islands algorithm to detect consecutive daily activity streaks and breaks in DuckDB.

README for GitHub

OutputREADME.md
# 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 detection
Stretch goal

Extend the query to find each user all-time maximum streak length and rank users on a global leaderboard.

Keep going