Gaps, islands & sessionization
Concept  ·  SQL & Querying

LAG gives each row its predecessor — the difference is the gap, and the first row's LAG is NULL by design.

Gap questions are self-join questions in disguise, and LAG is the self-join done for you: same partition, previous row by the ORDER BY. Two precision points carry the marks: the gap belongs to the row after the silence (the later match carries the long gap, not the earlier one) — off-by-one flaggings come from forgetting this; and the leading NULL means 'no predecessor', which most alerts should skip, not zero-fill.

See it break
matches
playerd
Ada1
Ada5
Ada40
The trap
SELECT
  player,
  d
FROM
  (
    SELECT
      player,
      d,
      LEAD (d) OVER (
        PARTITION BY
          player
        ORDER BY
          d
      ) - d AS gap
    FROM
      matches
  )
WHERE
  gap > 21
ORDER BY
  d;
The fix
SELECT
  player,
  d
FROM
  (
    SELECT
      player,
      d,
      d - LAG (d) OVER (
        PARTITION BY
          player
        ORDER BY
          d
      ) AS gap
    FROM
      matches
  )
WHERE
  gap > 21
ORDER BY
  d;
INPUTYour queryThe fix
Ada540

The absence alert flagged the match before the long gap instead of the one after it — LAG measures each row against its predecessor, and the first row's LAG is NULL by design.

The rule

Use d - LAG(d) OVER (PARTITION BY ... ORDER BY ...) and state the leading-NULL policy explicitly.

Shows up elsewhere
Sessionize by running-SUM of flagsThe islands trick
Try one →Prove it in practice →