← Назад к вопросам

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

2.3 Middle🔥 111 комментариев
#SQL и базы данных#Аналитика и метрики

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Сессионирование (sessionization) — это важная техника в аналитике, которая группирует события пользователя в логические сессии на основе временных промежутков. Если между двумя событиями одного пользователя прошло более 5 минут, они считаются разными сессиями.

Основные подходы

1. SQL решение с оконными функциями (самый распространённый)

Это решение работает в Spark SQL, PostgreSQL, Hive, Clickhouse и других SQL движках:

WITH ordered_events AS (
    -- Сортируем события каждого пользователя по времени
    SELECT 
        user_id,
        event_timestamp,
        LAG(event_timestamp) OVER (PARTITION BY user_id ORDER BY event_timestamp) AS prev_event_time
    FROM events
)
,
session_markers AS (
    -- Вычисляем разницу со предыдущим событием
    SELECT 
        user_id,
        event_timestamp,
        prev_event_time,
        -- Если разница больше 5 минут (300 секунд), это начало новой сессии
        CASE 
            WHEN prev_event_time IS NULL THEN 1
            WHEN (unix_timestamp(event_timestamp) - unix_timestamp(prev_event_time)) > 300 THEN 1
            ELSE 0
        END AS is_new_session
    FROM ordered_events
)
,
session_groups AS (
    -- Суммируем флаги, чтобы получить ID сессии
    SELECT 
        user_id,
        event_timestamp,
        is_new_session,
        SUM(is_new_session) OVER (PARTITION BY user_id ORDER BY event_timestamp) AS session_id
    FROM session_markers
)

SELECT 
    user_id,
    CONCAT(user_id, '_', session_id) AS session_uid,
    MIN(event_timestamp) AS session_start,
    MAX(event_timestamp) AS session_end,
    COUNT(*) AS events_in_session,
    DATEDIFF(SECOND, MIN(event_timestamp), MAX(event_timestamp)) / 60.0 AS session_duration_minutes
FROM session_groups
GROUP BY user_id, session_id
ORDER BY user_id, session_start;

2. PostgreSQL с временными интервалами

WITH events_with_gaps AS (
    SELECT 
        user_id,
        event_timestamp,
        event_type,
        -- Флаг: если разница со предыдущим событием > 5 минут
        CASE 
            WHEN LAG(event_timestamp) OVER (PARTITION BY user_id ORDER BY event_timestamp) IS NULL 
            THEN TRUE
            WHEN EXTRACT(EPOCH FROM (event_timestamp - LAG(event_timestamp) OVER (PARTITION BY user_id ORDER BY event_timestamp))) > 300
            THEN TRUE
            ELSE FALSE
        END AS gap_detected
    FROM events
),
session_ids AS (
    SELECT 
        user_id,
        event_timestamp,
        event_type,
        gap_detected,
        -- Генерируем ID сессии
        ROW_NUMBER() OVER (PARTITION BY user_id) - 
        SUM(CASE WHEN gap_detected THEN 1 ELSE 0 END) 
            OVER (PARTITION BY user_id ORDER BY event_timestamp) AS session_number
    FROM events_with_gaps
)

SELECT 
    user_id,
    session_number,
    COUNT(*) AS events,
    MIN(event_timestamp) AS session_start,
    MAX(event_timestamp) AS session_end,
    MAX(event_timestamp) - MIN(event_timestamp) AS duration,
    STRING_AGG(event_type, ', ' ORDER BY event_timestamp) AS events_sequence
FROM session_ids
GROUP BY user_id, session_number
ORDER BY user_id, session_start;

3. Spark Python решение

from pyspark.sql import SparkSession, Window
from pyspark.sql.functions import \
    col, lag, unix_timestamp, when, sum as spark_sum, \
    row_number, concat_ws, collect_list, first, last, count, min as spark_min, max as spark_max

spark = SparkSession.builder.getOrCreate()

# Предположим, у нас есть DataFrame events с колонками:
# user_id, event_timestamp, event_type

window = Window.partitionBy('user_id').orderBy('event_timestamp')

# Шаг 1: определяем границы сессий
events_with_gaps = events.withColumn(
    'prev_timestamp', lag('event_timestamp').over(window)
).withColumn(
    'gap_seconds', 
    when(
        col('prev_timestamp').isNotNull(),
        (unix_timestamp('event_timestamp') - unix_timestamp('prev_timestamp'))
    ).otherwise(0)
).withColumn(
    'is_new_session',
    when(col('gap_seconds') > 300, 1).otherwise(0)
)

# Шаг 2: генерируем ID сессии
session_ids = events_with_gaps.withColumn(
    'session_id',
    spark_sum('is_new_session').over(window)
)

# Шаг 3: агрегируем по сессиям
sessions = session_ids.groupBy('user_id', 'session_id').agg(
    spark_min('event_timestamp').alias('session_start'),
    spark_max('event_timestamp').alias('session_end'),
    count('*').alias('event_count'),
    collect_list('event_type').alias('events')
).orderBy('user_id', 'session_start')

sessions.show()

4. Hive SQL вариант

WITH raw_with_lags AS (
    SELECT 
        user_id,
        event_timestamp,
        event_type,
        LAG(event_timestamp, 1) OVER (PARTITION BY user_id ORDER BY event_timestamp) as prev_event_ts,
        (unix_timestamp(event_timestamp) - unix_timestamp(
            LAG(event_timestamp, 1) OVER (PARTITION BY user_id ORDER BY event_timestamp)
        )) / 60 as minutes_since_prev -- разница в минутах
    FROM events
    WHERE event_timestamp >= '2024-08-01' AND event_timestamp < '2024-09-01'
),
session_starts AS (
    SELECT 
        user_id,
        event_timestamp,
        event_type,
        CASE 
            WHEN prev_event_ts IS NULL THEN 1 -- первое событие
            WHEN minutes_since_prev >= 5 THEN 1 -- есть пауза >= 5 минут
            ELSE 0 
        END as session_start_flag
    FROM raw_with_lags
),
session_ids AS (
    SELECT 
        user_id,
        event_timestamp,
        event_type,
        SUM(session_start_flag) OVER (PARTITION BY user_id ORDER BY event_timestamp) as session_id
    FROM session_starts
)

SELECT 
    user_id,
    session_id,
    MIN(event_timestamp) as session_start,
    MAX(event_timestamp) as session_end,
    COUNT(*) as event_count,
    DATEDIFF(MAX(event_timestamp), MIN(event_timestamp)) as session_duration_minutes,
    COLLECT_LIST(event_type) as event_sequence
FROM session_ids
GROUP BY user_id, session_id
ORDER BY user_id, session_start;

Пример данных и результатов

Входные данные (events):

user_id | event_timestamp      | event_type
--------|----------------------|----------
  101   | 2024-08-30 10:00:00 | page_view
  101   | 2024-08-30 10:02:15 | click
  101   | 2024-08-30 10:04:30 | click
  101   | 2024-08-30 10:12:00 | page_view  -- 7.5 минут со предыдущего
  101   | 2024-08-30 10:13:00 | click
  102   | 2024-08-30 10:00:00 | page_view
  102   | 2024-08-30 10:01:00 | click

Результат (sessions):

user_id | session_id | session_start        | session_end          | event_count | duration_minutes
--------|------------|----------------------|----------------------|-------------|---------------
  101   |     1      | 2024-08-30 10:00:00 | 2024-08-30 10:04:30 |      3      |       4.5
  101   |     2      | 2024-08-30 10:12:00 | 2024-08-30 10:13:00 |      2      |       1
  102   |     1      | 2024-08-30 10:00:00 | 2024-08-30 10:01:00 |      2      |       1

Важные параметры

Таймаут 5 минут = 300 секунд

Другие популярные таймауты:

  • 30 минут = 1800 секунд (веб-сессии)
  • 15 минут = 900 секунд (мобильные приложения)
  • 10 минут = 600 секунд (средний уровень)

Оптимизация для больших объёмов

-- Для петабайт данных рекомендуется добавить партиционирование
WITH events AS (
    SELECT * 
    FROM raw_events
    WHERE ds >= '2024-08-01' AND ds <= '2024-08-31'  -- Партиция по дате
    AND user_id IS NOT NULL -- Фильтруем невалидные
)

-- Остальной SQL как выше...

Проверка качества сессий

-- Проверить, есть ли пропуски в сессиях
WITH sessions AS (
    -- Предыдущий запрос...
)
SELECT 
    user_id,
    session_id,
    MIN(session_start) as first_event,
    MAX(session_end) as last_event,
    COUNT(*) as num_events,
    (unix_timestamp(MAX(session_end)) - unix_timestamp(MIN(session_start))) / 60 as total_duration_minutes
FROM sessions
GROUP BY user_id, session_id
HAVING COUNT(*) < 3  -- Показать сессии с менее чем 3 событиями
ORDER BY user_id;

Выводы

Используйте оконные функции (LAG, SUM OVER) для определения границ сессий ✅ Таймаут 5 минут — стандарт для веб-аналитики ✅ Spark/SQL решение лучше масштабируется для Big Data ✅ Проверяйте качество результатов на выборке данных ✅ Партиционируйте по дате для оптимизации на больших объёмах

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