SQL (PostgreSQL, MySQL, SQLite) Module 8 – Window functions
Gaps and islands, streaks and sessions
Group consecutive rows into islands with the row-number difference, find gaps in sequences, and measure login streaks and sessions in SQL.
What you will learn
- Group consecutive rows into islands with the row-number difference
- Find the gaps in a numbered sequence with LEAD
- Measure the longest streak of consecutive active days per user
- Split events into sessions with LAG and a running sum
- Group rows into runs of the same value with two row numbers
Before you start
What you’ll learn · Before you start
What you will learn
- Group consecutive rows into islands with the row-number difference
- Find the gaps in a numbered sequence with LEAD
- Measure the longest streak of consecutive active days per user
- Split events into sessions with LAG and a running sum
- Group rows into runs of the same value with two row numbers
Before you start
On this page
Gaps and islands is the name of a family of SQL problems about runs of consecutive rows. An island is such a run:
the days in a row on which a user was active, invoice numbers with none missing, health checks that all said “up”.
Gaps are the holes between islands. Login streaks are islands of consecutive days, and sessions are islands of
events that come close together in time. One idea solves most of them: number the rows in order with row_number()
and subtract the number from the value. Inside an island both rise by one per row, so the difference stays the same,
and grouping by it returns one row per island with its first value, its last value and its length.
Why the value minus its row number finds the islands
Take one user who was active on days 1, 2, 3, 6, 7 and 10 of a month. The query runs on SQLite 3.49.1 in your browser:
-- The days of one month on which a user was active, as day numbers.
CREATE TABLE active (day INTEGER PRIMARY KEY);
INSERT INTO active VALUES (1), (2), (3), (6), (7), (10);
-- Number the days in order and subtract the number: within a run of consecutive days
-- both go up by 1, so the difference stays the same.
SELECT day,
row_number() OVER (ORDER BY day) AS rn,
day - row_number() OVER (ORDER BY day) AS island
FROM active
ORDER BY day;
-- One row per island: group by the difference.
WITH numbered AS (
SELECT day, day - row_number() OVER (ORDER BY day) AS island
FROM active
)
SELECT min(day) AS first_day, max(day) AS last_day, count(*) AS days
FROM numbered
GROUP BY island
ORDER BY first_day; Output
┌─────┬────┬────────┐ │ day │ rn │ island │ ├─────┼────┼────────┤ │ 1 │ 1 │ 0 │ │ 2 │ 2 │ 0 │ │ 3 │ 3 │ 0 │ │ 6 │ 4 │ 2 │ │ 7 │ 5 │ 2 │ │ 10 │ 6 │ 4 │ └─────┴────┴────────┘ ┌───────────┬──────────┬──────┐ │ first_day │ last_day │ days │ ├───────────┼──────────┼──────┤ │ 1 │ 3 │ 3 │ │ 6 │ 7 │ 2 │ │ 10 │ 10 │ 1 │ └───────────┴──────────┴──────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < islands_by_number.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
In the first result, row_number() counts the days from 1 to 6, and the difference is 0 for days 1 to 3, 2 for days
6 and 7, and 4 for day 10. It changes only where days are missing, and it grows by the number of days missed, so it
works as a label for each island. The second result groups by that label and finds three islands, of 3, 2 and 1
days. The gaps are what lies between them: days 4 and 5, and days 8 and 9. Nothing here is specific to one engine:
the pattern needs only row_number() and GROUP BY, so it runs in PostgreSQL, in MySQL 8.0 and later, in SQLite 3.25
and later, and in SQL Server and Oracle.
The day minus its row number is constant inside each island
Text description of the diagram
The diagram follows one user's active days, 1, 2, 3, 6, 7 and 10, from top to bottom.
1. The days are numbered in order with row_number(): 1 to 6. 2. Days 1, 2 and 3 have the row numbers 1, 2 and 3, so the day minus the row number is 0 for all three. They form the first island. 3. Days 4 and 5 are missing: the first gap. 4. Days 6 and 7 have the row numbers 4 and 5, and the difference is 2 for both: the second island. 5. Days 8 and 9 are missing: the second gap. 6. Day 10 has the row number 6, and the difference is 4: the third island, a single day. 7. Grouping by the difference returns one row per island: three islands of 3, 2 and 1 days.
The same subtraction, or a close relative of it, answers each problem of the family:
- Streaks of dates subtract the row number in days, with
date()in SQLite andday - nin PostgreSQL. - Duplicate days break the subtraction, so streaks must count each day once, with
DISTINCTordense_rank(). - Gaps in a numbered sequence show up where
lead()finds a next value more than 1 higher. - Sessions start after a pause longer than a limit, such as 30 minutes, and a running sum of those starts numbers them.
- Runs of the same status subtract two row numbers: one over all rows, and one within the status.
- Time zones decide which calendar day an event falls on, so streaks have to use the user’s own calendar.
Login streaks from dates, with the longest one per user Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Duplicate days split a streak: the failure, reproduced, and two fixes
- Gaps: the missing invoice numbers of each branch
- Sessions: a new session after 30 minutes without activity
- Runs of the same status, with two row numbers
- Whose day is it? Streaks in the user’s time zone
- How fast these queries run, and the index that removes the sort
- Your turn
- Each user's longest streak of active days
- Check yourself
- Interview questions
- Why does subtracting ROW_NUMBER from a column of consecutive values group the rows into islands?
- An events table stores every action of 5 million users in UTC, many per user per day. Return each user's longest daily streak and the streak running now, for users in India. Explain what can go wrong and how your query avoids it.
- An auditor wants every gap in the invoice numbers of each of 400 branches, about 40 million invoices in all, as ranges. Write the query, explain how you check it, and say what it cannot see.
- Split 2 billion page views into sessions that end after 30 minutes without a view, and report sessions per day with their average length. Which window functions do you need, and what goes wrong at midnight and with views in the same second?
- Key takeaways
6 runnable examples (SQL) · 1 exercise with sample tests · 3 interview answers · about 23 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- PostgreSQL documentation: Window Functions (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Date/Time Functions and Operators (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Set Returning Functions (generate_series) (The PostgreSQL Global Development Group)
- SQLite: Window Functions (SQLite)
- SQLite: Date And Time Functions (SQLite)
- SQLite: The WITH Clause (recursive common table expressions) (SQLite)
- MySQL 8.4 Reference Manual: Date and Time Functions (Oracle Corporation)
- MySQL 8.4 Reference Manual: Window Function Restrictions (Oracle Corporation)
- DATEADD (Transact-SQL) (Microsoft)
- DATEDIFF (Transact-SQL) (Microsoft)
- Oracle AI Database SQL Language Reference: Data Types (datetime arithmetic) (Oracle Corporation)
- About Analytics sessions (Analytics Help) (Google)
- Time zone database, Asia (tzdb source file) (Internet Assigned Numbers Authority (IANA))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress