lesson 54 Advanced product analysis · advanced
Sessionize an event stream
Group consecutive events into user sessions.
the idea
Sessionization groups each user’s events into sessions: a new session starts at the first event or after a gap of at least 30 minutes. Mark starts with LAG, then turn the marks into session numbers with a running SUM.
Dataset: Product / SaaS Analytics
The tables the challenges use, with their first 6 rows. Under each column name: the kind of value it holds, and which table it links to.
sessions5 columns · 47,891 rows
| session_idnumber · unique id | user_idnumber · links to users | started_atdate & time | ended_atdate & time | devicetext |
|---|---|---|---|---|
| 1 | 1 | 2024-01-14 18:52:14 | 2024-01-14 19:01:14 | web |
| 2 | 1 | 2024-01-21 11:24:15 | 2024-01-21 11:37:15 | web |
| 3 | 2 | 2024-01-24 03:43:14 | 2024-01-24 03:46:14 | android |
| 4 | 1 | 2024-02-02 07:45:53 | 2024-02-02 08:22:53 | web |
| 5 | 3 | 2024-02-02 21:34:25 | 2024-02-02 21:49:25 | web |
| 6 | 4 | 2024-03-31 03:39:32 | 2024-03-31 03:44:32 | web |
- session_idnumber · unique id
- 1
- user_idnumber · links to users
- 1
- started_atdate & time
- 2024-01-14 18:52:14
- ended_atdate & time
- 2024-01-14 19:01:14
- devicetext
- web
- session_idnumber · unique id
- 2
- user_idnumber · links to users
- 1
- started_atdate & time
- 2024-01-21 11:24:15
- ended_atdate & time
- 2024-01-21 11:37:15
- devicetext
- web
- session_idnumber · unique id
- 3
- user_idnumber · links to users
- 2
- started_atdate & time
- 2024-01-24 03:43:14
- ended_atdate & time
- 2024-01-24 03:46:14
- devicetext
- android
- session_idnumber · unique id
- 4
- user_idnumber · links to users
- 1
- started_atdate & time
- 2024-02-02 07:45:53
- ended_atdate & time
- 2024-02-02 08:22:53
- devicetext
- web
- session_idnumber · unique id
- 5
- user_idnumber · links to users
- 3
- started_atdate & time
- 2024-02-02 21:34:25
- ended_atdate & time
- 2024-02-02 21:49:25
- devicetext
- web
- session_idnumber · unique id
- 6
- user_idnumber · links to users
- 4
- started_atdate & time
- 2024-03-31 03:39:32
- ended_atdate & time
- 2024-03-31 03:44:32
- devicetext
- web
first 6 of 47,891 rows · your queries use all of them
events8 columns · 135,428 rows
| delivery_idnumber · unique id | event_idtext | user_idnumber · links to users | account_idnumber · links to accounts | session_idnumber · links to sessions | event_nametext | occurred_atdate & time | received_atdate & time |
|---|---|---|---|---|---|---|---|
| 1 | evt_0000001 | 1 | 1 | 1 | user_signed_up | 2024-01-14 18:52:14 | 2024-01-14 18:52:33 |
| 2 | evt_0000002 | 1 | 1 | 1 | onboarding_started | 2024-01-14 18:52:51 | 2024-01-14 18:53:06 |
| 3 | evt_0000084 | 6 | 2 | 32 | user_signed_up | 2024-01-15 13:21:52 | 2024-01-15 13:22:00 |
| 4 | evt_0000085 | 6 | 2 | 32 | onboarding_started | 2024-01-15 13:22:05 | 2024-01-15 13:22:16 |
| 5 | evt_0000086 | 6 | 2 | 32 | onboarding_completed | 2024-01-15 13:23:52 | 2024-01-15 13:24:12 |
| 6 | evt_0000019 | 1 | 1 | 9 | search_performed | 2024-01-17 20:56:55 | 2024-01-17 20:56:59 |
- delivery_idnumber · unique id
- 1
- event_idtext
- evt_0000001
- user_idnumber · links to users
- 1
- account_idnumber · links to accounts
- 1
- session_idnumber · links to sessions
- 1
- event_nametext
- user_signed_up
- occurred_atdate & time
- 2024-01-14 18:52:14
- received_atdate & time
- 2024-01-14 18:52:33
- delivery_idnumber · unique id
- 2
- event_idtext
- evt_0000002
- user_idnumber · links to users
- 1
- account_idnumber · links to accounts
- 1
- session_idnumber · links to sessions
- 1
- event_nametext
- onboarding_started
- occurred_atdate & time
- 2024-01-14 18:52:51
- received_atdate & time
- 2024-01-14 18:53:06
- delivery_idnumber · unique id
- 3
- event_idtext
- evt_0000084
- user_idnumber · links to users
- 6
- account_idnumber · links to accounts
- 2
- session_idnumber · links to sessions
- 32
- event_nametext
- user_signed_up
- occurred_atdate & time
- 2024-01-15 13:21:52
- received_atdate & time
- 2024-01-15 13:22:00
- delivery_idnumber · unique id
- 4
- event_idtext
- evt_0000085
- user_idnumber · links to users
- 6
- account_idnumber · links to accounts
- 2
- session_idnumber · links to sessions
- 32
- event_nametext
- onboarding_started
- occurred_atdate & time
- 2024-01-15 13:22:05
- received_atdate & time
- 2024-01-15 13:22:16
- delivery_idnumber · unique id
- 5
- event_idtext
- evt_0000086
- user_idnumber · links to users
- 6
- account_idnumber · links to accounts
- 2
- session_idnumber · links to sessions
- 32
- event_nametext
- onboarding_completed
- occurred_atdate & time
- 2024-01-15 13:23:52
- received_atdate & time
- 2024-01-15 13:24:12
- delivery_idnumber · unique id
- 6
- event_idtext
- evt_0000019
- user_idnumber · links to users
- 1
- account_idnumber · links to accounts
- 1
- session_idnumber · links to sessions
- 9
- event_nametext
- search_performed
- occurred_atdate & time
- 2024-01-17 20:56:55
- received_atdate & time
- 2024-01-17 20:56:59
first 6 of 135,428 rows · your queries use all of them
worked example
Run these steps in order to see the idea before you write your own query. The step marked “the pattern” is what the challenges ask for. Running the example does not count toward progress.
Measure idle time between events. Run this. LAG() on occurred_at shows minutes since user 100’s previous event—the input to a 30-minute session rule.
step-1.sqlSELECT user_id, occurred_at, ROUND((julianday(occurred_at) - julianday(LAG(occurred_at) OVER (PARTITION BY user_id ORDER BY occurred_at, event_id))) * 1440, 1) AS minutes_since_previous FROM events WHERE user_id = 100;
Compare to stored sessions. Run this. The sessions table is the product’s own log for the same user—your SQL rebuild should resemble these boundaries.
step-2.sqlSELECT session_id, started_at, ended_at FROM sessions WHERE user_id = 100;
Mark where a session starts. Run this. starts_session is 1 on the first event or after a gap of at least 30 minutes; a running sum of those flags numbers sessions next.
step-3.sqlWITH g AS ( SELECT user_id, occurred_at, (julianday(occurred_at) - julianday(LAG(occurred_at) OVER (PARTITION BY user_id ORDER BY occurred_at, event_id))) * 1440 AS gap FROM events WHERE user_id = 100 ) SELECT occurred_at, ROUND(gap, 1) AS minutes_since_previous, CASE WHEN gap IS NULL OR gap >= 30 THEN 1 ELSE 0 END AS starts_session FROM g;
Put it together. The lesson's pattern in one query.the pattern
example.sqlSELECT user_id, occurred_at, LAG(occurred_at) OVER (PARTITION BY user_id ORDER BY occurred_at, event_id) AS previous_at FROM events WHERE user_id = 100;
common mistake
Ordering by received_at instead of occurred_at splits sessions when late deliveries arrive out of order.