0% completed
Gaps and Islands
On This Page
The Table
Islands and Gaps
Subtracting a Row Number
Comparing a Row With the One Before It
Flagging and Summing
Repeated Reports
Reports at Uneven Intervals
Choosing a Form
Practice: Find the Longest Uninterrupted Run
Using This in an Interview
Key Takeaways
Practice Questions
Here are the reporting slots of one solar inverter on one morning: 1, 2, 3, 4, 7, 8, 10, 11, 12. How many separate outages did this inverter have?
The inverter sends a short status message every 15 minutes to say that it is running and connected (a heartbeat). Slot 1 is 10:00, slot 2 is 10:15, and so on, so a missing slot number means a missing report. Reading the list, slots 5 and 6 are missing, and then slot 9. That is two outages, one of two slots and one of a single slot.
With many inverters, the count has to come from SQL. This lesson shows two ways to write it, and the cases in which each one returns a wrong answer.
The Table
Each row of heartbeats is one report. slot_no numbers the 15-minute reporting intervals from 10:00, and slot_time shows the same interval as clock time.
| inverter_id | slot_no | slot_time |
|---|---|---|
| INV-7 | 1 | 10:00 |
| INV-7 | 2 | 10:15 |
| INV-7 | 3 | 10:30 |
| INV-7 | 4 | 10:45 |
| INV-7 | 7 | 11:30 |
| INV-7 | 8 | 11:45 |
| INV-7 | 10 | 12:15 |
| INV-7 | 11 | 12:30 |
| INV-7 | 12 | 12:45 |
| INV-9 | 1 | 10:00 |
| INV-9 | 2 | 10:15 |
| INV-9 | 3 | 10:30 |
| INV-9 | 5 | 11:00 |
| INV-9 | 6 | 11:15 |
Every query below was run on MySQL 8.4, and the output shown is the real output.
Islands and Gaps
An island is a run of rows whose sequence values follow each other with no value missing, and a gap is a stretch of missing values between two islands. Inverter INV-7 has three islands, slots 1 to 4, 7 to 8, and 10 to 12, with a gap between each pair. Each gap is one outage, so once the islands are known, the outages are known too.
A plain SQL expression reads one row, so it cannot tell whether the previous slot was reported. Two forms answer that with window functions, and on clean data both return the same islands.
Subtracting a Row Number
The subtraction form labels each island by subtracting a row number from the sequence value, which gives one result for every row of the island. It uses ROW_NUMBER, which numbers the rows of a partition 1, 2, 3 (Ranking and Ties compares it with RANK and DENSE_RANK). Inside an island, the slot number and the row number both rise by 1 on every row, so their difference stays the same. After a gap, the slot number jumps ahead while the row number does not, so the difference grows.
SELECT slot_no, slot_time, ROW_NUMBER() OVER (ORDER BY slot_no) AS rn, slot_no - ROW_NUMBER() OVER (ORDER BY slot_no) AS island_key FROM heartbeats WHERE inverter_id = 'INV-7' ORDER BY slot_no;
Output:
+---------+-----------+----+------------+ | slot_no | slot_time | rn | island_key | +---------+-----------+----+------------+ | 1 | 10:00 | 1 | 0 | | 2 | 10:15 | 2 | 0 | | 3 | 10:30 | 3 | 0 | | 4 | 10:45 | 4 | 0 | | 7 | 11:30 | 5 | 2 | | 8 | 11:45 | 6 | 2 | | 10 | 12:15 | 7 | 3 | | 11 | 12:30 | 8 | 3 | | 12 | 12:45 | 9 | 3 | +---------+-----------+----+------------+
Slots 1 to 4 give 0, slots 7 and 8 give 2, and slots 10 to 12 give 3. The value means nothing by itself, but every row of one island shares it, so it works as a group label (an island key). It rose by 2 after slot 4 because two slots were missing, and by 1 more after slot 8.
Grouping by the key turns each island into one row, and PARTITION BY numbers each inverter on its own:
WITH keyed AS ( SELECT inverter_id, slot_no, slot_no - ROW_NUMBER() OVER ( PARTITION BY inverter_id ORDER BY slot_no ) AS island_key FROM heartbeats ) SELECT inverter_id, MIN(slot_no) AS first_slot, MAX(slot_no) AS last_slot, COUNT(*) AS slots FROM keyed GROUP BY inverter_id, island_key ORDER BY inverter_id, first_slot;
Output:
+-------------+------------+-----------+-------+ | inverter_id | first_slot | last_slot | slots | +-------------+------------+-----------+-------+ | INV-7 | 1 | 4 | 4 | | INV-7 | 7 | 8 | 2 | | INV-7 | 10 | 12 | 3 | | INV-9 | 1 | 3 | 3 | | INV-9 | 5 | 6 | 2 | +-------------+------------+-----------+-------+
INV-7 has three islands and INV-9 has two, each now one row.
Comparing a Row With the One Before It
LAG is a window function that returns a value from an earlier row of the same partition, by default the row immediately before the current one. The MySQL manual states that when no such row exists, as for the first row, LAG returns NULL unless a default value is given. The difference between a slot and the slot before it is the step, and a step of 1 means that no report was missed.
SELECT slot_no, LAG(slot_no) OVER (ORDER BY slot_no) AS prev_slot, slot_no - LAG(slot_no) OVER (ORDER BY slot_no) AS step FROM heartbeats WHERE inverter_id = 'INV-7' ORDER BY slot_no;
Output:
+---------+-----------+------+ | slot_no | prev_slot | step | +---------+-----------+------+ | 1 | NULL | NULL | | 2 | 1 | 1 | | 3 | 2 | 1 | | 4 | 3 | 1 | | 7 | 4 | 3 | | 8 | 7 | 1 | | 10 | 8 | 2 | | 11 | 10 | 1 | | 12 | 11 | 1 | +---------+-----------+------+
The step is 3 at slot 7, because slots 5 and 6 are missing, and 2 at slot 10, because slot 9 is missing. Every step above 1 ends a gap, so the gaps can be listed directly:
WITH stepped AS ( SELECT inverter_id, slot_no, LAG(slot_no) OVER ( PARTITION BY inverter_id ORDER BY slot_no ) AS prev_slot FROM heartbeats ) SELECT inverter_id, prev_slot + 1 AS first_missed, slot_no - 1 AS last_missed, slot_no - prev_slot - 1 AS missed_slots FROM stepped WHERE slot_no - prev_slot > 1 ORDER BY inverter_id, first_missed;
Output:
+-------------+--------------+-------------+--------------+ | inverter_id | first_missed | last_missed | missed_slots | +-------------+--------------+-------------+--------------+ | INV-7 | 5 | 6 | 2 | | INV-7 | 9 | 9 | 1 | | INV-9 | 4 | 4 | 1 | +-------------+--------------+-------------+--------------+
This answers the opening question. INV-7 had two outages, slots 5 to 6 (11:00 to 11:15) and slot 9 (12:00), and INV-9 had one, slot 4.
Flagging and Summing
The flag form marks each row that starts an island with a 1, and then numbers the islands with a running sum of those marks. First, each row gets a flag of 1 if it starts a new island and 0 if it continues one. Second, a running total of the flags (the ROWS running total from Window Functions) gives each row the number of islands started so far.
WITH flagged AS ( SELECT inverter_id, slot_no, CASE WHEN slot_no - LAG(slot_no) OVER ( PARTITION BY inverter_id ORDER BY slot_no) <= 1 THEN 0 ELSE 1 END AS is_new FROM heartbeats ) SELECT inverter_id, slot_no, is_new, SUM(is_new) OVER ( PARTITION BY inverter_id ORDER BY slot_no ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS island_no FROM flagged WHERE inverter_id = 'INV-7' ORDER BY slot_no;
Output:
+-------------+---------+--------+-----------+ | inverter_id | slot_no | is_new | island_no | +-------------+---------+--------+-----------+ | INV-7 | 1 | 1 | 1 | | INV-7 | 2 | 0 | 1 | | INV-7 | 3 | 0 | 1 | | INV-7 | 4 | 0 | 1 | | INV-7 | 7 | 1 | 2 | | INV-7 | 8 | 0 | 2 | | INV-7 | 10 | 1 | 3 | | INV-7 | 11 | 0 | 3 | | INV-7 | 12 | 0 | 3 | +-------------+---------+--------+-----------+
The first row has no previous slot, so its step is NULL, the <= 1 test fails, and the flag is 1. The flags 1, 0, 0, 0, 1, 0, 1, 0, 0 add up to island numbers 1, 1, 1, 1, 2, 2, 3, 3, 3. Grouping by inverter_id and island_no returns the same five islands as the subtraction form.
The flag form is longer, but its rule for a new island is an ordinary CASE expression, so the rule can change while the rest of the query stays the same.
Repeated Reports
Suppose INV-7 sends its 12:30 report twice. After the INSERT below, the islands query from the subtraction form runs again unchanged and returns this:
INSERT INTO heartbeats VALUES ('INV-7', 11, '12:30');
Output:
+-------------+------------+-----------+-------+ | inverter_id | first_slot | last_slot | slots | +-------------+------------+-----------+-------+ | INV-7 | 1 | 4 | 4 | | INV-7 | 7 | 12 | 4 | | INV-7 | 10 | 11 | 2 | | INV-9 | 1 | 3 | 3 | | INV-9 | 5 | 6 | 2 | +-------------+------------+-----------+-------+
One run now claims slots 7 to 12, across the slot 9 outage, and a second run from 10 to 11 overlaps it. The MySQL manual gives the cause: ROW_NUMBER assigns rows with equal ORDER BY values different numbers. The two slot 11 rows take row numbers 8 and 9, so one gets key 3 and the other key 2. Slot 12 takes row number 10 and also gets key 2, the same key as slots 7 and 8.
Duplicates can also stop the query. When the slots start at 1, a repeat before the first gap pushes the row number past the slot number, so the difference falls below zero. MySQL returns the row number as an unsigned integer, and the subtraction fails with error 1690, "BIGINT UNSIGNED value is out of range".
DENSE_RANK gives equal values the same number, without gaps, so it counts distinct slots instead of rows. With DENSE_RANK and a count of distinct slots, the three islands come back:
WITH keyed AS ( SELECT inverter_id, slot_no, slot_no - DENSE_RANK() OVER ( PARTITION BY inverter_id ORDER BY slot_no ) AS island_key FROM heartbeats ) SELECT inverter_id, MIN(slot_no) AS first_slot, MAX(slot_no) AS last_slot, COUNT(DISTINCT slot_no) AS slots, COUNT(*) AS reports FROM keyed GROUP BY inverter_id, island_key ORDER BY inverter_id, first_slot;
Output:
+-------------+------------+-----------+-------+---------+ | inverter_id | first_slot | last_slot | slots | reports | +-------------+------------+-----------+-------+---------+ | INV-7 | 1 | 4 | 4 | 4 | | INV-7 | 7 | 8 | 2 | 2 | | INV-7 | 10 | 12 | 3 | 4 | | INV-9 | 1 | 3 | 3 | 3 | | INV-9 | 5 | 6 | 2 | 2 | +-------------+------------+-----------+-------+---------+
The last island of INV-7 has 3 slots but 4 reports. The flag form needs no change here, because a repeated slot has a step of 0 and does not start an island. With COUNT(DISTINCT slot_no), it returns the same islands. A third option is to remove repeats with SELECT DISTINCT before either form.
Reports at Uneven Intervals
An older inverter, INV-3, does not report in fixed slots. It reports about every 15 minutes by the clock, and its reports drift by a minute or two. Its table, old_heartbeats, stores minutes since midnight: 600, 615, 631, 645, 661, 720, 736, and 750.
The subtraction form needs values that rise by exactly 1 inside an island, and these do not. Subtracting 15 times the row number gives 600 - 15 = 585 and 615 - 30 = 585, but then 631 - 45 = 586, so one island splits into several. The flag form only needs a different test: a report more than 20 minutes after the previous one starts a new island.
WITH flagged AS ( SELECT minute_no, CASE WHEN minute_no - LAG(minute_no) OVER (ORDER BY minute_no) <= 20 THEN 0 ELSE 1 END AS is_new FROM old_heartbeats ), numbered AS ( SELECT minute_no, SUM(is_new) OVER ( ORDER BY minute_no ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS island_no FROM flagged ) SELECT island_no, MIN(minute_no) AS first_minute, MAX(minute_no) AS last_minute, COUNT(*) AS reports FROM numbered GROUP BY island_no ORDER BY island_no;
Output:
+-----------+--------------+-------------+---------+ | island_no | first_minute | last_minute | reports | +-----------+--------------+-------------+---------+ | 1 | 600 | 661 | 5 | | 2 | 720 | 750 | 3 | +-----------+--------------+-------------+---------+
The 59-minute step from 661 to 720 is the only one above 20, so INV-3 had one outage. The 20-minute limit is a choice, not a fact in the data. A smaller limit turns every late report into an outage, and a larger one hides short outages.
Choosing a Form
| Slot minus row number | Flag, then running sum | |
|---|---|---|
| Windows | One | Two (LAG, then SUM) |
| Repeated values | Needs DENSE_RANK | Works, count distinct slots |
| Values that do not step by 1 | Gives wrong islands | Any rule in the CASE |
| Lists the gaps | No | Yes, from the LAG step |
- Subtraction form: shortest when values step by exactly 1. Slot numbers and day numbers fit. Its cost: a repeat or an uneven step breaks it, and the wrong result can look normal.
- Flag form: needed when the rule for a new island is not "plus 1". Drift or a time limit goes into the CASE. Its cost is a second window and a second CTE.
- Both forms: decide what a repeated value means before counting. Count distinct slots to measure time online, and count rows only when every report matters.
Practice: Find the Longest Uninterrupted Run
For each inverter in heartbeats, return its longest run of consecutive reporting slots. Return inverter_id, first_slot, last_slot, and slots, the number of distinct slots in the run. An inverter can report twice for the same slot: count that slot once, and do not let the repeat break the run. When two runs are equally long, return the one that starts first. An inverter with a single report has a run of 1 slot. Sort the result by inverter_id.
Using This in an Interview
Before writing, ask two questions out loud: does the sequence column step by exactly 1, and can a value repeat? The answers decide the form. While writing the subtraction form, state why it works: "Inside an island, the slot and the row number rise together, so their difference is constant." Two follow-up questions are worth preparing for:
- "How would you list only outages of an hour or more?" An hour is 4 slots, so filter the gap query on
slot_no - prev_slot - 1 >= 4. - "Can your query see an outage before the first report?" No, because LAG has no row before the first one. Joining to a full list of expected slots, such as the recursive day spine in CTEs and Recursive CTEs, makes those missing slots visible.
Key Takeaways
- An island is a run of values with nothing missing, and a gap is the stretch of missing values between two islands.
- Subtracting ROW_NUMBER from a value that steps by 1 gives a key that is constant within each island, so GROUP BY on that key returns one row per island.
- LAG returns the previous row's value, and the step from it marks where each gap ends.
- Flagging each new island and summing the flags numbers the islands under any rule that a CASE expression can state.
- Repeated values break the ROW_NUMBER form; use DENSE_RANK and count distinct values, or remove repeats first.
The flag form accepts any rule for where a new run begins. With the rule "more than 30 minutes since the previous event", the islands become user sessions. The next lesson, Sessionizing Events, builds them from page views on a recipe website.
Practice Questions
1. An inverter reports slots 3, 4, 5, 9, 10, and 14. What island key does slot_no - ROW_NUMBER() give each row, and how many islands are there?
The keys are 2, 2, 2, 5, 5, and 8, so there are three islands. The row numbers are 1 to 6, and subtracting them gives 3 - 1 = 2 for slots 3 to 5, 9 - 4 = 5 for slots 9 and 10, and 14 - 6 = 8 for slot 14. The islands are slots 3 to 5, 9 to 10, and 14 alone.
</details>2. The same inverter's steps from LAG are NULL, 1, 1, 4, 1, and 4. How many slots did it miss in total?
<details> <summary>Show answer</summary>Six slots. Each gap misses the step minus 1 slots. The two steps of 4 each miss 3 slots, slots 6 to 8 and slots 11 to 13, and the steps of 1 miss nothing.
</details>3. An inverter reports slots 1, 2, 2, and 3. What does the ROW_NUMBER form do on MySQL, and how is it fixed?
<details> <summary>Show answer</summary>It fails with error 1690, because the third row computes 2 - 3, which is below zero for an unsigned row number. Even where the result stays at or above zero, a repeat shifts the keys of every later row. Using DENSE_RANK gives the repeated slot the same number, so the keys are 0, 0, 0, and 0, and counting distinct slots reports one island of 3 slots.
</details>4. A sensor reports every 10 minutes by the clock, but each report can arrive up to 2 minutes late. Which form fits, and what would the rule be?
<details> <summary>Show answer</summary>The flag form, with a time limit in the CASE. The values do not step by a fixed amount, so the subtraction form would split islands. A rule such as "a step of more than 15 minutes starts a new island" accepts late reports and still detects a missed report, since one missed report makes the step at least 18 minutes.
</details>5. The islands query returns three islands for one inverter on one day. How many outages does that show?
<details> <summary>Show answer</summary>Two outages, one between each pair of neighboring islands. In general, n islands have n - 1 gaps between them. Outages before the first report or after the last one are not counted, because no row marks them, so a full count needs the list of expected slots.
</details>Reading Progress
0%
On This Page
The Table
Islands and Gaps
Subtracting a Row Number
Comparing a Row With the One Before It
Flagging and Summing
Repeated Reports
Reports at Uneven Intervals
Choosing a Form
Practice: Find the Longest Uninterrupted Run
Using This in an Interview
Key Takeaways
Practice Questions