runtime
54 / 88

lesson 54 Advanced product analysis · advanced

Sessionize an event stream

Group consecutive events into user sessions.

the idea

A session starts on a member’s first event or after a gap of at least 30 minutes. LAG finds the prior timestamp, CASE flags boundaries, and a running SUM numbers sessions. Here we use physical rows, including retries. Identical timestamps do not start a new session.

tables in this lesson

The challenges use this data. Each column shows its type underneath; → marks a link to another table.

social_events6 columns · 22 rows
idinteger · keyevent_keytextmember_idintegerevent_typetextevent_attextingested_attext
1a1view2025-01-01 10:00:002025-01-01 10:00:01
2b1signup2025-01-01 10:03:002025-01-01 10:03:01
3c1purchase2025-01-01 10:05:002025-01-01 10:05:01
4a1view2025-01-01 10:00:002025-01-01 10:10:00
5d2view2025-01-02 09:00:002025-01-02 09:00:01

showing 5 of 22 rows

example

The idea in one query. Press Run to see what it returns; running examples never affects your progress.

example.sql
WITH previous AS(
  SELECT
    *,
    LAG(event_at) OVER(PARTITION BY member_id ORDER BY event_at,id) AS previous_at
  FROM social_events
), marked AS(
  SELECT
    *,
    CASE
      WHEN previous_at IS NULL
        OR unixepoch(event_at)-unixepoch(previous_at)>=1800
        THEN 1
      ELSE 0
    END AS new_session
  FROM previous
), numbered AS(
  SELECT
    *,
    SUM(new_session) OVER(PARTITION BY member_id ORDER BY event_at,id ROWS UNBOUNDED PRECEDING) AS session_number
  FROM marked
)
SELECT member_id,MAX(session_number) AS session_count
FROM numbered
GROUP BY member_id;

Watch for: Strictly greater than 30 minutes is not the same as at least 30 minutes.

build it step by step

Each step changes one thing. Run them in order and watch the result change.

  1. Find the previous timestamp. Order within member. Raw retries remain in this exercise.

    step-1.sql
    SELECT
      id,
      member_id,
      event_at,
      LAG(event_at) OVER(PARTITION BY member_id ORDER BY event_at,id) AS previous_at
    FROM social_events
    ORDER BY member_id,event_at,id;
  2. Number the sessions. A new boundary occurs on the first event or a gap >=1800 seconds.

    step-2.sql
    WITH previous AS(
      SELECT
        *,
        LAG(event_at) OVER(PARTITION BY member_id ORDER BY event_at,id) AS previous_at
      FROM social_events
    ), marked AS(
      SELECT
        *,
        CASE
          WHEN previous_at IS NULL
            OR unixepoch(event_at)-unixepoch(previous_at)>=1800
            THEN 1
          ELSE 0
        END AS new_session
      FROM previous
    ), numbered AS(
      SELECT
        *,
        SUM(new_session) OVER(PARTITION BY member_id ORDER BY event_at,id ROWS UNBOUNDED PRECEDING) AS session_number
      FROM marked
    )
    SELECT id,member_id,session_number
    FROM numbered
  3. Summarize sessions per member. The largest session number is the total number of sessions.

    step-3.sql
    WITH previous AS(
      SELECT
        *,
        LAG(event_at) OVER(PARTITION BY member_id ORDER BY event_at,id) AS previous_at
      FROM social_events
    ), marked AS(
      SELECT
        *,
        CASE
          WHEN previous_at IS NULL
            OR unixepoch(event_at)-unixepoch(previous_at)>=1800
            THEN 1
          ELSE 0
        END AS new_session
      FROM previous
    ), numbered AS(
      SELECT
        *,
        SUM(new_session) OVER(PARTITION BY member_id ORDER BY event_at,id ROWS UNBOUNDED PRECEDING) AS session_number
      FROM marked
    )
    SELECT member_id,MAX(session_number) AS session_count
    FROM numbered
    GROUP BY member_id

challenge 1

Using raw social_events, return member_id and session_count. Start a new session on the first event or a gap of at least 30 minutes; order by event_at then id per member.

Return member_idsession_countany row order

challenge-1.sql
 
To master this lesson, solve both challenges without hints and pass the concept check. 3 of 3 to go.