This lesson on Consecutive Days, Sessions — Gaps and Islands is hands-on and example-driven. You will master solving advanced gaps and islands problems involving overlapping, touching, and fully enclosed date ranges using window functions. By the end of this lesson, you will be able to construct multi-stage queries using LAG, cumulative conditional sums, and aggregate grouping to merge fragmented time intervals into unified session ranges.
What You'll Be Able To Do
- Identify continuous sequences and boundary gaps within interval-based datasets.
- Retrieve prior interval boundaries using the LAG window function ordered by interval start and end times.
- Flag interval transition boundaries using conditional CASE logic based on boundary overlaps.
- Generate persistent island group identifiers using a cumulative SUM window function.
- Aggregate grouped interval partitions into consolidated start and end ranges using MIN and MAX.
Detailed Concept Walkthrough
1. Interval Gaps and Islands Concept
An island represents an unbroken, continuous block of sequential or overlapping records, whereas a gap is a break or missing interval between those blocks. When dealing with date intervals, islands can contain rows that touch, overlap on one boundary, or are fully enclosed within an earlier record.
- Mechanism: Unlike discrete sequential ID gaps, date interval islands require evaluating whether a current interval's start boundary begins before or at the maximum previous end boundary. Any interval starting strictly after the preceding interval's end boundary marks the end of the previous island and the birth of a gap.
- Execution Flow: The dataset is ordered chronologically by start date and end date to ensure that each record is evaluated against the immediately preceding timeline context.
- Best Practice: Always inspect whether boundary dates are inclusive or exclusive; identical start and end points between adjacent rows determine whether they merge into one island or start a new island.
-- Inspecting raw interval records that contain overlaps and enclosures
SELECT
row_id,
start_date,
end_date
FROM interval_data
ORDER BY start_date, end_date;
Key Takeaway: Overlapping or enclosed ranges are part of the same island as long as no chronological gap exists between them.
2. Boundary Comparison with LAG
The LAG window function pulls the end boundary of the preceding row into the current row's evaluation context to detect boundary disconnects.
- Mechanism: By specifying LAG(end_date) OVER (ORDER BY start_date, end_date), the query exposes the prior interval's closing timestamp to the current row without requiring a self-join. If the current row's start date exceeds this lagged end date (or if the lagged value is NULL), a new island has started.
- Under the Hood: SQL Server evaluates the window frame across the ordered partition in memory, scanning rows sequentially to fetch previous row attributes at minimal computational cost.
- Syntax Rule: The ORDER BY clause inside LAG must match the physical sequence of your intervals, sorting primarily on start_date and secondarily on end_date to maintain deterministic ordering.
-- Stage 1: Assign sequencing and fetch the prior row's end boundary
SELECT
start_date,
end_date,
ROW_NUMBER() OVER (ORDER BY start_date, end_date) AS row_num,
LAG(end_date) OVER (ORDER BY start_date, end_date) AS prev_end_date
FROM interval_data;
Key Takeaway: LAG brings the previous row's boundary into the current row, enabling direct row-to-row gap detection.
3. Island Identification via Cumulative Sum
A binary indicator marks the start of each island, and a cumulative running sum of these indicators creates a unique, grouping-ready identifier for every row.
- Mechanism: A CASE statement evaluates whether start_date is greater than prev_end_date; it returns 1 for a new island (or the table's first row) and 0 for continuous/overlapping rows. A running SUM of this binary indicator increments by 1 only when a gap occurs, remaining constant across all records belonging to the same island.
- Execution Flow: The cumulative SUM function processes rows in chronological sequence, effectively partitioning the records into discrete numeric buckets.
- Nuance: Fully enclosed intervals (where both start and end fall inside a previous range) maintain an indicator of 0 because their start date is prior to the active range's boundary.
-- Stage 2: Create binary start indicator and compute running island ID
WITH lagged_data AS (
SELECT
start_date,
end_date,
ROW_NUMBER() OVER (ORDER BY start_date, end_date) AS row_num,
LAG(end_date) OVER (ORDER BY start_date, end_date) AS prev_end_date
FROM interval_data
)
SELECT
start_date,
end_date,
CASE
WHEN prev_end_date IS NULL OR start_date > prev_end_date THEN 1
ELSE 0
END AS is_island_start,
SUM(CASE WHEN prev_end_date IS NULL OR start_date > prev_end_date THEN 1 ELSE 0 END)
OVER (ORDER BY row_num) AS island_id
FROM lagged_data;
Key Takeaway: Running sums over conditional binary indicators transform boundary break events into groupable island IDs.
4. Interval Aggregation and Consolidation
Grouping by the computed island ID and aggregating the start and end columns merges fragmented intervals into single consolidated rows.
- Mechanism: Using GROUP BY island_id with MIN(start_date) and MAX(end_date) collapses all overlapping, enclosed, and adjacent records within each partition into one summary record covering the entire span.
- Under the Hood: The database engine hashes or streams rows by island_id, reducing multiple detailed rows down to exact island boundaries in a single aggregation step.
- Best Practice: Encapsulate intermediate logic in Common Table Expressions (CTEs) or derived tables to keep the final SELECT statement clean and readable for reporting layers.
-- Stage 3: Consolidate intervals into final island ranges
WITH lagged_data AS (
SELECT
start_date,
end_date,
ROW_NUMBER() OVER (ORDER BY start_date, end_date) AS row_num,
LAG(end_date) OVER (ORDER BY start_date, end_date) AS prev_end_date
FROM interval_data
),
island_groups AS (
SELECT
start_date,
end_date,
SUM(CASE WHEN prev_end_date IS NULL OR start_date > prev_end_date THEN 1 ELSE 0 END)
OVER (ORDER BY row_num) AS island_id
FROM lagged_data
)
SELECT
island_id,
MIN(start_date) AS island_start_date,
MAX(end_date) AS island_end_date
FROM island_groups
GROUP BY island_id
ORDER BY island_start_date;
Key Takeaway: Grouping on island_id with MIN and MAX collapses complex overlapping fragments into unified timeline spans.
Topics Covered in Consecutive Days, Sessions — Gaps and Islands
- Introduction to Gaps and Islands (0:00 - 0:45) — The playing card analogy illustrates sequential island groups and missing gap elements.
- Date Range Overlap Complexities (0:45 - 1:30) — Sample data highlights challenges with overlapping and fully enclosed interval records.
- Lag Function and Row Numbering (1:30 - 2:15) — Row numbering and the LAG function capture the previous interval boundary.
- Island Indicator and Running Total (2:15 - 2:55) — A conditional start indicator combined with a running SUM assigns group IDs.
- Aggregation into Island Ranges (2:55 - 3:40) — Grouping by island ID with MIN and MAX consolidates fragmented date ranges.
SQL Cheat Sheet
-
LAG(column) OVER (ORDER BY ...)— Fetches a value from the preceding row within an ordered setLAG(end_date) OVER (ORDER BY start_date, end_date) -
ROW_NUMBER() OVER (ORDER BY ...)— Generates a sequential integer for each row based on orderingROW_NUMBER() OVER (ORDER BY start_date, end_date) -
CASE WHEN start > prev_end THEN 1 ELSE 0 END— Flags interval break points as binary start indicatorsCASE WHEN start_date > prev_end_date THEN 1 ELSE 0 END -
SUM(flag) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)— Computes cumulative running total to assign incremental island IDsSUM(is_island_start) OVER (ORDER BY row_num) -
MIN(start_date) ... MAX(end_date) GROUP BY island_id— Collapses grouped island records into unified start and end boundariesSELECT island_id, MIN(start_date), MAX(end_date) GROUP BY island_id
Comparison Table
| Relationship Type | Boundary Condition | Island Behavior |
|---|---|---|
| Continuous / Touching | Current start equals previous end | Merged into existing island |
| Overlapping Boundary | Current start before previous end | Merged into existing island |
| Fully Enclosed | Both boundaries inside previous range | Merged into existing island |
| Disconnected Gap | Current start after previous end | Triggers new island start |
Common Pitfalls
- Mistake: Ordering window functions only by start date when end dates vary. Avoid: Sort deterministically on both start_date and end_date.
- Mistake: Using self-joins instead of window functions for gap detection. Avoid: Use LAG and cumulative SUM to prevent exponential join explosion.
- Mistake: Forgetting to handle NULL on the first row lagged value. Avoid: Treat NULL lagged boundaries as an automatic island start indicator.
FAQs
- Why is this problem harder than traditional discrete gaps and islands? Discrete problems evaluate single integer steps, whereas date interval problems must handle overlapping boundaries and fully enclosed time spans.
- Is the binary island start indicator strictly required in production queries? No, you can embed the CASE condition directly inside the cumulative SUM window function without creating an intermediate column.
- What happens if an interval is completely contained within an earlier interval? Its start date will be earlier than the previous end date, correctly assigning it an indicator of 0 to keep it in the same island.