Clickstream Sessionization with Inactivity Gaps
1. Analytical Significance & What It Means
Scenario & Frontier TrapGroups web analytics events into browsing sessions where an inactivity gap greater than 30 minutes demarcates a new session boundary.
Sessionization with inactivity timeouts is a classic Gaps & Islands problem. Naive LLMs routinely attempt to group directly by timestamps or confuse ROW_NUMBER() with session grouping, failing to calculate the cumulative sum of step flags (LAG condition). The optimal pattern requires a two-step window CTE: step 1 identifies boundary leaps (> 30 min) with LAG(), and step 2 computes a running cumulative sum of those flags to assign unique session IDs.
You are given a table of raw web clickstream events `user_events`. Group each user's events into distinct browsing sessions. A new session starts when: 1. It is the user's very first event, OR 2. More than 30 minutes (DuckDB: INTERVAL 30 MINUTE) have elapsed since the user's immediately preceding event. For each user and session, return: - user_id (INT) - session_id (BIGINT or INT): A sequential integer session index per user starting at 1 - session_start (TIMESTAMP): Timestamp of the first event in the session - session_end (TIMESTAMP): Timestamp of the last event in the session - event_count (BIGINT or INT): Total number of events in this session Sort the results by user_id ASC, session_start ASC.
2. Underlying Tables, Row Counts & Data Volume
1 Tables (1,000,000 rows • 34.3 MiB)Production-scale fixtures loaded into the isolated DuckDB sandbox with at least 1,000,000 records per table:
event_id (INTEGER) user_id (INTEGER) event_timestamp (TIMESTAMP) page_url (VARCHAR)| # | event_id | user_id | event_timestamp | page_url |
|---|---|---|---|---|
| 1 | 1 | 10 | 2025-01-01 10:00:00 | /home |
| 2 | 2 | 10 | 2025-01-01 10:15:00 | /pricing |
| 3 | 3 | 10 | 2025-01-01 11:00:00 | /docs |
| 4 | 4 | 10 | 2025-01-01 11:10:00 | /checkout |
| 5 | 5 | 20 | 2025-01-01 09:00:00 | /blog |
| 6 | 5 | 20 | 2026-03-01 09:30:00.000005 | /home |
| 7 | 6 | 21 | 2026-03-01 09:30:00.000036 | /products/detail |
| 8 | 7 | 22 | 2026-03-01 09:30:00.000051 | /cart |
| 9 | 8 | 23 | 2026-03-01 09:30:00.000071 | /checkout |
| 10 | 9 | 24 | 2026-03-01 09:30:00.000082 | /search |
| 11 | 10 | 25 | 2026-03-01 09:30:00.000114 | /account |
| 12 | 11 | 26 | 2026-03-01 09:30:00.000123 | /help |
| 13 | 12 | 27 | 2026-03-01 09:30:00.000148 | /home |
| 14 | 13 | 28 | 2026-03-01 09:30:00.000168 | /products/detail |
| 15 | 14 | 29 | 2026-03-01 09:30:00.000189 | /cart |
3. Graphical Query Execution Plan & Cost Bottlenecks
Physical Operator Tree (DuckDB 2.0 Profiler)Visual flowchart of the physical execution pipeline. Operators with high relative costs (≥ 20% of query runtime or heavy latency) are visually highlighted:
| Operator | Cost % | Latency | Processed Rows | Data Volume | Details / Table | Status |
|---|---|---|---|---|---|---|
| HASH_GROUP_BY | 37.2% | 290.33 ms | 999,998 | 41.96 MB | - | 🔥 BOTTLENECK |
| WINDOW | 27.8% | 217.21 ms | 1,000,000 | 30.52 MB | Projections: sum_no_overflow(is_new_session) OVE | 🔥 BOTTLENECK |
| WINDOW | 19.1% | 149.37 ms | 1,000,000 | 19.19 MB | Projections: lag(event_timestamp, 1, NULL::TIMES | 🔥 BOTTLENECK |
| ORDER_BY | 15.3% | 119.67 ms | 999,998 | 41.96 MB | - | 🔥 BOTTLENECK |
| PROJECTION | 0.3% | 2.68 ms | 1,000,000 | 15.26 MB | Projections: ['user_id', 'event_timestamp', 'is_ | Normal |
| TABLE_SCAN | 0.1% | 1.07 ms | 1,000,000 | 11.44 MB | memory.main.user_events | Normal |
| PROJECTION | 0.0% | 0.35 ms | 1,000,000 | 30.52 MB | Projections: ['#0', '#1', '#2', '#3'] | Normal |
| PROJECTION | 0.0% | 0.26 ms | 1,000,000 | 34.33 MB | Projections: ['user_id', 'session_id', 'event_ti | Normal |
| PROJECTION | 0.0% | 0.21 ms | 1,000,000 | 26.70 MB | Projections: ['user_id', 'event_timestamp', 'ses | Normal |
| PROJECTION | 0.0% | 0.14 ms | 1,000,000 | 26.95 MB | Projections: ['#0', '#1', '#2', '#2'] | Normal |
4. Queries Executed (Candidate vs Golden Ground Truth)
Beautified & Syntax HighlightedWITH lagged_events AS
(SELECT user_id,
event_timestamp,
CASE
WHEN event_timestamp - lag(event_timestamp) OVER (PARTITION BY user_id
ORDER BY event_timestamp) > INTERVAL 30 MINUTE THEN 1
WHEN lag(event_timestamp) OVER (PARTITION BY user_id
ORDER BY event_timestamp) IS NULL THEN 1
ELSE 0
END AS is_new_session
FROM user_events),
session_tagged AS
(SELECT user_id,
event_timestamp,
sum(is_new_session) OVER (PARTITION BY user_id
ORDER BY event_timestamp ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS session_id
FROM lagged_events)
SELECT user_id,
session_id,
min(event_timestamp) AS session_start,
max(event_timestamp) AS session_end,
count(*) AS event_count
FROM session_tagged
GROUP BY user_id,
session_id
ORDER BY user_id ASC,
session_start ASC;
WITH lagged AS
(SELECT user_id,
event_timestamp,
CASE
WHEN event_timestamp - lag(event_timestamp) OVER (PARTITION BY user_id
ORDER BY event_timestamp) > INTERVAL 30 MINUTE
OR lag(event_timestamp) OVER (PARTITION BY user_id
ORDER BY event_timestamp) IS NULL THEN 1
ELSE 0
END AS is_new_session
FROM user_events),
session_indexed AS
(SELECT user_id,
event_timestamp,
cast(sum(is_new_session) OVER (PARTITION BY user_id
ORDER BY event_timestamp ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BIGINT) AS session_id
FROM lagged)
SELECT user_id,
session_id,
min(event_timestamp) AS session_start,
max(event_timestamp) AS session_end,
count(*) AS event_count
FROM session_indexed
GROUP BY user_id,
session_id
ORDER BY user_id ASC,
session_start ASC;
CREATE TABLE user_events (event_id INT, user_id INT, event_timestamp TIMESTAMP, page_url VARCHAR);
INSERT INTO user_events
VALUES (1, 10, '2025-01-01 10:00:00', '/home'),
(2, 10, '2025-01-01 10:15:00', '/pricing'),
(3, 10, '2025-01-01 11:00:00', '/docs'),
(4, 10, '2025-01-01 11:10:00', '/checkout'),
(5, 20, '2025-01-01 09:00:00', '/blog');
5. Query Results & Expected Ground Truth
Tabular Comparison| # | user_id | session_id | session_start | session_end | event_count |
|---|---|---|---|---|---|
| 1 | 10 | 1 | 2025-01-01 10:00:00 | 2025-01-01 10:15:00 | 2 |
| 2 | 10 | 2 | 2025-01-01 11:00:00 | 2025-01-01 11:10:00 | 2 |
| 3 | 20 | 1 | 2025-01-01 09:00:00 | 2025-01-01 09:00:00 | 1 |
| 4 | 20 | 2 | 2026-03-01 09:30:00.000005 | 2026-03-01 09:30:00.000005 | 1 |
| 5 | 21 | 1 | 2026-03-01 09:30:00.000036 | 2026-03-01 09:30:00.000036 | 1 |
| 6 | 22 | 1 | 2026-03-01 09:30:00.000051 | 2026-03-01 09:30:00.000051 | 1 |
| 7 | 23 | 1 | 2026-03-01 09:30:00.000071 | 2026-03-01 09:30:00.000071 | 1 |
| 8 | 24 | 1 | 2026-03-01 09:30:00.000082 | 2026-03-01 09:30:00.000082 | 1 |
| 9 | 25 | 1 | 2026-03-01 09:30:00.000114 | 2026-03-01 09:30:00.000114 | 1 |
| 10 | 26 | 1 | 2026-03-01 09:30:00.000123 | 2026-03-01 09:30:00.000123 | 1 |
| 11 | 27 | 1 | 2026-03-01 09:30:00.000148 | 2026-03-01 09:30:00.000148 | 1 |
| 12 | 28 | 1 | 2026-03-01 09:30:00.000168 | 2026-03-01 09:30:00.000168 | 1 |
| 13 | 29 | 1 | 2026-03-01 09:30:00.000189 | 2026-03-01 09:30:00.000189 | 1 |
| 14 | 30 | 1 | 2026-03-01 09:30:00.000202 | 2026-03-01 09:30:00.000202 | 1 |
| 15 | 31 | 1 | 2026-03-01 09:30:00.000231 | 2026-03-01 09:30:00.000231 | 1 |
| 16 | 32 | 1 | 2026-03-01 09:30:00.000246 | 2026-03-01 09:30:00.000246 | 1 |
| 17 | 33 | 1 | 2026-03-01 09:30:00.000262 | 2026-03-01 09:30:00.000262 | 1 |
| 18 | 34 | 1 | 2026-03-01 09:30:00.000286 | 2026-03-01 09:30:00.000286 | 1 |
| 19 | 35 | 1 | 2026-03-01 09:30:00.000303 | 2026-03-01 09:30:00.000303 | 1 |
| 20 | 36 | 1 | 2026-03-01 09:30:00.000321 | 2026-03-01 09:30:00.000321 | 1 |
| 21 | 37 | 1 | 2026-03-01 09:30:00.000343 | 2026-03-01 09:30:00.000343 | 1 |
| 22 | 38 | 1 | 2026-03-01 09:30:00.000361 | 2026-03-01 09:30:00.000361 | 1 |
| 23 | 39 | 1 | 2026-03-01 09:30:00.000390 | 2026-03-01 09:30:00.000390 | 1 |
| 24 | 40 | 1 | 2026-03-01 09:30:00.000405 | 2026-03-01 09:30:00.000405 | 1 |
| 25 | 41 | 1 | 2026-03-01 09:30:00.000421 | 2026-03-01 09:30:00.000421 | 1 |
| # | user_id | session_id | session_start | session_end | event_count |
|---|---|---|---|---|---|
| 1 | 10 | 1 | 2025-01-01 10:00:00 | 2025-01-01 10:15:00 | 2 |
| 2 | 10 | 2 | 2025-01-01 11:00:00 | 2025-01-01 11:10:00 | 2 |
| 3 | 20 | 1 | 2025-01-01 09:00:00 | 2025-01-01 09:00:00 | 1 |
| 4 | 20 | 2 | 2026-03-01 09:30:00.000005 | 2026-03-01 09:30:00.000005 | 1 |
| 5 | 21 | 1 | 2026-03-01 09:30:00.000036 | 2026-03-01 09:30:00.000036 | 1 |
| 6 | 22 | 1 | 2026-03-01 09:30:00.000051 | 2026-03-01 09:30:00.000051 | 1 |
| 7 | 23 | 1 | 2026-03-01 09:30:00.000071 | 2026-03-01 09:30:00.000071 | 1 |
| 8 | 24 | 1 | 2026-03-01 09:30:00.000082 | 2026-03-01 09:30:00.000082 | 1 |
| 9 | 25 | 1 | 2026-03-01 09:30:00.000114 | 2026-03-01 09:30:00.000114 | 1 |
| 10 | 26 | 1 | 2026-03-01 09:30:00.000123 | 2026-03-01 09:30:00.000123 | 1 |
| 11 | 27 | 1 | 2026-03-01 09:30:00.000148 | 2026-03-01 09:30:00.000148 | 1 |
| 12 | 28 | 1 | 2026-03-01 09:30:00.000168 | 2026-03-01 09:30:00.000168 | 1 |
| 13 | 29 | 1 | 2026-03-01 09:30:00.000189 | 2026-03-01 09:30:00.000189 | 1 |
| 14 | 30 | 1 | 2026-03-01 09:30:00.000202 | 2026-03-01 09:30:00.000202 | 1 |
| 15 | 31 | 1 | 2026-03-01 09:30:00.000231 | 2026-03-01 09:30:00.000231 | 1 |
| 16 | 32 | 1 | 2026-03-01 09:30:00.000246 | 2026-03-01 09:30:00.000246 | 1 |
| 17 | 33 | 1 | 2026-03-01 09:30:00.000262 | 2026-03-01 09:30:00.000262 | 1 |
| 18 | 34 | 1 | 2026-03-01 09:30:00.000286 | 2026-03-01 09:30:00.000286 | 1 |
| 19 | 35 | 1 | 2026-03-01 09:30:00.000303 | 2026-03-01 09:30:00.000303 | 1 |
| 20 | 36 | 1 | 2026-03-01 09:30:00.000321 | 2026-03-01 09:30:00.000321 | 1 |
| 21 | 37 | 1 | 2026-03-01 09:30:00.000343 | 2026-03-01 09:30:00.000343 | 1 |
| 22 | 38 | 1 | 2026-03-01 09:30:00.000361 | 2026-03-01 09:30:00.000361 | 1 |
| 23 | 39 | 1 | 2026-03-01 09:30:00.000390 | 2026-03-01 09:30:00.000390 | 1 |
| 24 | 40 | 1 | 2026-03-01 09:30:00.000405 | 2026-03-01 09:30:00.000405 | 1 |
| 25 | 41 | 1 | 2026-03-01 09:30:00.000421 | 2026-03-01 09:30:00.000421 | 1 |