Как выделить пользовательские сессии с таймаутом 5 минут на SQL?

«Как выделить пользовательские сессии с таймаутом 5 минут на SQL?» — вопрос из категории Аналитика и метрики, который задают на 33% собеседований Data Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для выделения сессий по таймауту неактивности я использую оконные функции, которые сравнивают время между последовательными событиями пользователя. Ключевая логика: новая сессия начинается, если разница с предыдущим событием превышает 5 минут (300 секунд) или это первое событие пользователя.

Детальное SQL-решение (на диалекте BigQuery, но принцип универсален):

WITH user_events AS (
  -- Исходные данные: user_id, event_timestamp
  SELECT 
    user_id,
    TIMESTAMP '2024-08-30 10:00:00' AS event_timestamp UNION ALL
  SELECT 'user1', TIMESTAMP '2024-08-30 10:01:00' UNION ALL
  SELECT 'user1', TIMESTAMP '2024-08-30 10:07:00' UNION ALL -- Разница >5 мин, новая сессия
  SELECT 'user2', TIMESTAMP '2024-08-30 10:00:00'
),
session_boundaries AS (
  SELECT 
    user_id,
    event_timestamp,
    -- Флаг начала новой сессии: если перерыв >300 сек или это первая запись пользователя
    CASE 
      WHEN TIMESTAMP_DIFF(
            event_timestamp, 
            LAG(event_timestamp) OVER (PARTITION BY user_id ORDER BY event_timestamp), 
            SECOND
           ) > 300 
      THEN 1
      WHEN LAG(event_timestamp) OVER (PARTITION BY user_id ORDER BY event_timestamp) IS NULL 
      THEN 1
      ELSE 0
    END AS is_new_session_flag
  FROM user_events
),
session_groups AS (
  SELECT 
    user_id,
    event_timestamp,
    -- Накопительная сумма флагов создает уникальный ID сессии для каждого пользователя
    SUM(is_new_session_flag) OVER (PARTITION BY user_id ORDER BY event_timestamp) AS session_id
  FROM session_boundaries
)
-- Финальная агрегация по сессиям
SELECT 
  user_id,
  session_id,
  MIN(event_timestamp) AS session_start,
  MAX(event_timestamp) AS session_end,
  TIMESTAMP_DIFF(MAX(event_timestamp), MIN(event_timestamp), SECOND) AS session_duration_sec,
  COUNT(*) AS events_in_session
FROM session_groups
GROUP BY user_id, session_id
ORDER BY user_id, session_start;
Результат для примера: user_id session_id session_start session_end session_duration_sec events_in_session
user1 1 2024-08-30 10:00:00 UTC 2024-08-30 10:01:00 UTC 60 2
user1 2 2024-08-30 10:07:00 UTC 2024-08-30 10:07:00 UTC 0 1
user2 1 2024-08-30 10:00:00 UTC 2024-08-30 10:00:00 UTC 0 1

Пояснение:

  1. LAG() получает время предыдущего события для того же пользователя.
  2. TIMESTAMP_DIFF() вычисляет разницу в секундах.
  3. Условие > 300 (5 минут) определяет таймаут.
  4. SUM(...) OVER (...) работает как cumsum() — накапливает флаги начала сессии, создавая уникальный session_id в рамках пользователя.