SQL

SQL Gaps & Islands Problems

Identify continuous and missing values in a sequence

Posted by Yvonne Liu on October 20, 2019

Description of Gaps & Islands Problems

Gaps and islands problems involve missing values in a sequence. Solving the gaps problem requires finding the ranges of missing values, whereas solving the islands problem involves finding the ranges of existing values.

The sequences of values in gaps and islands problems can be numeric, such as a sequence of order IDs, some of which were deleted. An example of the gaps problem in this case would be finding the ranges of deleted order IDs. An example of the islands problem would be finding the ranges of existing IDs.

The sequences involved can also be temporal, such as order dates, some of which are missing due to inactive periods (weekends, holidays). Finding periods of inactivity is an example of the gaps problem, and finding periods of activity is an example of the islands problem. Another example of a temporal sequence is a process that needs to report every fixed interval of time that it is online (for example, every 4 hours). Finding unavailability and availability periods is another example of gaps and islands problems.

Sample Data & Desired Results

Here, we have two tables - Accounts & Logins. We want to identify the id and name of active users who logged in to their accounts for 5 or more consecutive days.

Accounts table:

id name
1 Winston
7 Jonathan
  • id is the primary key for this table.
  • This table contains the account id and the user name of each account.

Logins table:

id login_date
7 2020-05-30
1 2020-05-30
7 2020-05-31
7 2020-06-01
7 2020-06-02
7 2020-06-02
7 2020-06-03
1 2020-06-07
7 2020-06-10
  • There is no primary key for this table, it may contain duplicates.
  • This table contains the account id of the user who logged in and the login date. A user may log in multiple times in the day.

Constructing the Query

WITH c1 AS 
(
SELECT      id, 
            login_date, 
            DENSE_RANK() OVER(PARTITION BY id ORDER BY login_date) AS rank1
FROM        Logins
ORDER BY    id, login_date
),

c2 AS 
(
SELECT      id, 
            login_date, 
            rank1, 
            DATE_SUB(login_date, INTERVAL rank1 DAY) as sequence_grouping
FROM        c1
),

c3 AS 
(
SELECT      id, 
            MIN(login_date) AS start_date, 
            MAX(login_date) AS end_date, 
            sequence_grouping, 
            COUNT(DISTINCT login_date) as consecutive_days
FROM        c2
GROUP BY    id, sequence_grouping
HAVING      COUNT(DISTINCT login_date) >= 5
)

SELECT      DISTINCT c3.id, name
FROM        c3
INNER JOIN  Accounts as a
ON          a.id = c3.id
ORDER BY    c3.id

The first CTE (c1) will return a result table like this:

id login_date rank1
1 2020-05-30 1
1 2020-06-07 2
7 2020-05-30 1
7 2020-05-31 2
7 2020-06-01 3
7 2020-06-02 4
7 2020-06-02 4
7 2020-06-03 5
7 2020-06-10 6
  • Here, we use a DENSE_RANK() because the Logins table has duplicates. We can also use ROW_NUMBER() if there are no duplicates.

In the second CTE (c2), we use DATE_SUB() to subtract the rank1 number from login_date, which results in the following table.

id login_date rank1 sequence_grouping
1 2020-05-30 1 2020-05-29
1 2020-06-07 2 2020-06-05
7 2020-05-30 1 2020-05-29
7 2020-05-31 2 2020-05-29
7 2020-06-01 3 2020-05-29
7 2020-06-02 4 2020-05-29
7 2020-06-02 4 2020-05-29
7 2020-06-03 5 2020-05-29
7 2020-06-10 6 2020-06-04
  • The sequence_grouping will be the same for all records in the same “island”.

In the third CTE (c3), we have the start_date (when the user begins to login consecutively), end_date, and consecutive_days (number of days user login consecutively).

id start_date end_date sequence_grouping consecutive_days
7 2020-05-30 2020-06-03 2020-05-29 5

Finally, by joining to the Accounts table, we have the final result table:

id name
7 Jonathan

Using this solution, We could identify active users who logged in to their accounts for n or more days by simply changing the HAVING clause in c3.