Gaps and islands

Find unbroken runs of consecutive values ("islands") and the holes between them ("gaps") — login streaks, missing invoice numbers, contiguous date ranges.

The problem

You have a sequence — dates, integers, IDs — and you need to group the consecutive stretches, or find where the sequence skips.

The trick

For a run of consecutive values, value − ROW_NUMBER() is constant. When the sequence jumps, that difference jumps too. Group by the difference and each group is one island.

Islands: contiguous stretches

monthly_revenue has months 2026-01 … 2026-04 with no gap, so it's one island. This finds the start, end, and length of every run of consecutive order_ids per customer:

SQL playground
Loading editor…
⌘/Ctrl + Enter

Gaps: what's missing

Compare each value to the previous one with LAG; a jump greater than 1 is a gap:

SQL playground
Loading editor…
⌘/Ctrl + Enter

(The seed order_ids are 101–111 with no gaps, so this returns nothing — delete a row in your head and it would.)

Dates instead of integers

Same idea, but subtract a row number of days so the units line up:

SELECT user_id, MIN(login_date) AS streak_start, MAX(login_date) AS streak_end,
       COUNT(*) AS days
FROM (
  SELECT user_id, login_date,
    date(login_date, '-' ||
      ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) || ' days') AS grp
  FROM logins
) t
GROUP BY user_id, grp;

Postgres: login_date - (ROW_NUMBER() OVER (...))::int gives the same anchor date.

  • ROW_NUMBER — the counter that makes the difference constant.
  • LAG / LEAD — comparing each row to its neighbour for the gap version.
  • Running totals — another "look at the neighbouring rows" pattern.

On this page