A running sum of 0/1 flags is a counter that only advances at boundaries — every row between two boundaries inherits the same count, which is exactly a session id. The leading NULL gap folds in for free (NULL → flag 1: the first event starts session 1). The two classic failures are both partition failures: omit PARTITION BY on either window and one entity's counter continues into the next; or order by the wrong column and boundaries land mid-burst.
WITH flagged AS ( SELECT volunteer, ts, CASE WHEN ts - LAG (ts) OVER ( ORDER BY ts ) > INTERVAL '2 hours' OR LAG (ts) OVER ( ORDER BY ts ) IS NULL THEN 1 ELSE 0 END AS new_session FROM obs ) SELECT volunteer, strftime(ts, '%H:%M') AS mark, SUM(new_session) OVER ( ORDER BY ts ) AS session_id FROM flagged ORDER BY ts;
WITH flagged AS ( SELECT volunteer, ts, CASE WHEN ts - LAG (ts) OVER ( PARTITION BY volunteer ORDER BY ts ) > INTERVAL '2 hours' OR LAG (ts) OVER ( PARTITION BY volunteer ORDER BY ts ) IS NULL THEN 1 ELSE 0 END AS new_session FROM obs ) SELECT volunteer, strftime(ts, '%H:%M') AS mark, SUM(new_session) OVER ( PARTITION BY volunteer ORDER BY ts ) AS session_id FROM flagged ORDER BY volunteer, ts;
Without PARTITION BY, one volunteer's session count ran straight into the next volunteer's rows — sessionization needs both windows partitioned per entity or the sessions bleed across them.
The two-step build (gap → flag → running SUM), both windows partitioned per entity, with the timeout as a named interval, not a magic number.