Gaps, islands & sessionization
Concept  ·  SQL & Querying

The first event of a session and sessions spanning midnight are the classic off-by-one traps (P12 crossover).

Every threshold predicate owns its boundary, and sessionization makes the ownership visible: 'a session ends after 30 minutes of silence' — is silence of exactly 30 minutes enough? Both answers are defensible; shipping one by accident of operator choice is the bug, because downstream metrics shift at every boundary-equal gap — and those are common when events are minute-rounded (rounding concentrates mass on the threshold).

See it break
pings
t
0
30
The trap
SELECT
  SUM(
    CASE
      WHEN gap >= 30 THEN 1
      ELSE 0
    END
  ) AS new_sessions
FROM
  (
    SELECT
      t - LAG (t) OVER (
        ORDER BY
          t
      ) AS gap
    FROM
      pings
  )
WHERE
  gap IS NOT NULL;
The fix
SELECT
  SUM(
    CASE
      WHEN gap > 30 THEN 1
      ELSE 0
    END
  ) AS new_sessions
FROM
  (
    SELECT
      t - LAG (t) OVER (
        ORDER BY
          t
      ) AS gap
    FROM
      pings
  )
WHERE
  gap IS NOT NULL;
The trap
1
The fix
0

A gap of exactly 30 minutes started a new session under '>=' but not '>' — the exactly-timeout gap is decided by one character, nobody chose, and downstream session counts shift; the boundary must be stated, not inherited from the operator.

The rule

The chosen operator plus a comment stating the rule in English; an exact-boundary row in the test fixture, always.

Shows up elsewhere
Sessionize by running-SUM of flagsCASE takes the first match
Try one →Prove it in practice →