Relix

Problem solving

In a row, per visit, gaps

Grain: one row per event (labelled), or one per session · Class: Sequence · Signals: in a row, per visit, consecutive, gaps, since last, streak, the next one · Operators: SESSIONIZE, WINDOW LAG/LEAD

The problem

"Group these page views into visits that end after 30 minutes idle, and count each visit. And by how much did each daily meter reading change from the one before?"

How to recognise it

The question is about a row's position in an ordered stream and its neighbours — in a row, consecutive, per visit, gaps between, since last time, streak, the next one. It is not about a key you can group on directly; the grouping emerges from the order. Two operators cover it:

Both add a column and keep every row, so they compose with the rest of the query.

Recipe 1: split a stream into sessions (SESSIONIZE)

A visit is a run of events with no long gap. SESSIONIZE orders each partition, and starts a new session whenever the gap to the previous row exceeds the threshold.

Query
EventsRaw := [
| user | ts                   |
|------|----------------------|
| 1    | 2026-01-01T10:00:00Z |
| 1    | 2026-01-01T10:20:00Z |
| 1    | 2026-01-01T11:30:00Z |
| 2    | 2026-01-01T09:00:00Z |
];

Events   := { π user, to_timestamp(ts) → ts (EventsRaw) };
Sessions := { SESSIONIZE ts GAP DURATION 'PT30M' PER user AS session (Events) };
query { Sessions };
Result
 user  ts                    session
 ────  ────────────────────  ───────
    1  2026-01-01T10:00:00Z        1
    1  2026-01-01T10:20:00Z        1
    1  2026-01-01T11:30:00Z        2
    2  2026-01-01T09:00:00Z        1
(4 rows)

The session id is only useful once you aggregate over it — how many visits, how long each is an ordinary γ keyed on the emergent (user, session) grain:

Query
query { γ user, session, COUNT(ts) → events, MIN(ts) → started, MAX(ts) → ended (Sessions) };
Result
 user  session  events  started               ended
 ────  ───────  ──────  ────────────────────  ────────────────────
    1        1       2  2026-01-01T10:00:00Z  2026-01-01T10:20:00Z
    1        2       1  2026-01-01T11:30:00Z  2026-01-01T11:30:00Z
    2        1       1  2026-01-01T09:00:00Z  2026-01-01T09:00:00Z
(3 rows)

Recipe 2: compare a row with its neighbour (LAG/LEAD)

Change since last looks back one row: LAG(reading) brings the previous row's value alongside the current one, so a subtraction gives the delta. The first row of a partition has no predecessor, so its LAG is NULL.

Query
Meter := [
| day        | reading |
|------------|---------|
| 2026-01-01 | 100     |
| 2026-01-02 | 130     |
| 2026-01-03 | 125     |
];

WithPrev := { WINDOW LAG(reading) SORT day ASC AS prev (Meter) };
query { π day, reading, reading - prev → change (WithPrev) };
Result
 day         reading  change
 ──────────  ───────  ──────
 2026-01-01      100  NULL
 2026-01-02      130      30
 2026-01-03      125      -5
(3 rows)

LEAD is the mirror — it looks forward to the next row, for time until the next event or the value that follows:

Query
query { WINDOW LEAD(reading) SORT day ASC AS next_reading (Meter) };
Result
 day         reading  next_reading
 ──────────  ───────  ────────────
 2026-01-01      100           130
 2026-01-02      130           125
 2026-01-03      125  NULL
(3 rows)

Variations

Pitfalls

Check it