Grokking the Data Engineering Interview
Vote

0% completed

​

Gaps and Islands

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_idslot_noslot_time
INV-7110:00
INV-7210:15
INV-7310:30
INV-7410:45
INV-7711:30
INV-7811:45
INV-71012:15
INV-71112:30
INV-71212:45
INV-9110:00
INV-9210:15
INV-9310:30
INV-9511:00
INV-9611: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.

Slot minus row number marks each island
Slot minus row number marks each island

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.

Flag each new run, then add up the flags
Flag each new run, then add up the flags

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 numberFlag, then running sum
WindowsOneTwo (LAG, then SUM)
Repeated valuesNeeds DENSE_RANKWorks, count distinct slots
Values that do not step by 1Gives wrong islandsAny rule in the CASE
Lists the gapsNoYes, 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.

MYSQL
MYSQL

. . . .

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?

<details> <summary>Show answer</summary>

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%


Vote for new content

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