Как посчитать time to value в SQL

Проверь себя · 1/3разбор после ответа
Нужно получить колонку event_date типа date из created_at (тип timestamp) для JOIN с календарной таблицей по дате. Какое выражение подойдёт лучше всего?

Что такое TTV

Time to Value (TTV) — время от регистрации до первого получения ценности от продукта. Чем короче TTV, тем сильнее удержание. Это одна из главных метрик активации.

В Slack — время до первого отправленного сообщения в команду. В Notion — до первой созданной страницы. В банковском приложении — до первого перевода.

Не путать с активацией: активация — доля пользователей, дошедших до ценности. TTV — время, за которое они доходят.

Выбор события «value»

Это самая важная часть. Хорошее value-событие:

  • Связано с ключевой ценностью продукта, не вторичным действием
  • Сильно коррелирует с удержанием на D7 (как и активация)
  • Происходит у большинства активных пользователей (иначе нет данных для медианы)

Например, для маркетплейса доставки:

  • Плохой выбор: «открыл каталог» (происходит у всех, не отражает ценности)
  • Хороший: «получил первый заказ»
  • Отличный: «получил первый заказ и оценил его на 4-5 звёзд»

SQL: общий TTV

Допустим, у вас есть users(user_id, signup_at) и events(user_id, event_type, created_at):

WITH first_value AS (
  SELECT
    u.user_id,
    u.signup_at,
    MIN(e.created_at) AS first_value_at
  FROM users u
  LEFT JOIN events e
    ON e.user_id = u.user_id
    AND e.event_type = 'first_order_completed'
    AND e.created_at >= u.signup_at
  WHERE u.signup_at >= '2026-01-01'
  GROUP BY u.user_id, u.signup_at
)
SELECT
  COUNT(*) AS total_users,
  COUNT(first_value_at) AS reached_value,
  ROUND(COUNT(first_value_at)::NUMERIC / COUNT(*), 3) AS share_reached,
  EXTRACT(EPOCH FROM AVG(first_value_at - signup_at)) / 3600 AS avg_ttv_hours,
  EXTRACT(EPOCH FROM PERCENTILE_CONT(0.5)
    WITHIN GROUP (ORDER BY first_value_at - signup_at)) / 3600 AS median_ttv_hours
FROM first_value
WHERE first_value_at IS NOT NULL;

Медиана почти всегда полезнее среднего — TTV имеет длинный хвост (некоторые пользователи активируются через месяц и тянут среднее).

Закрепи формулу time to value в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать time to value в Telegram

SQL: TTV по когортам

Чтобы видеть, как TTV меняется со временем (после релизов, изменений в онбординге):

WITH first_value AS (
  SELECT
    u.user_id,
    DATE_TRUNC('week', u.signup_at)::DATE AS cohort_week,
    u.signup_at,
    MIN(e.created_at) AS first_value_at
  FROM users u
  LEFT JOIN events e
    ON e.user_id = u.user_id
    AND e.event_type = 'first_order_completed'
    AND e.created_at >= u.signup_at
  GROUP BY u.user_id, cohort_week, u.signup_at
)
SELECT
  cohort_week,
  COUNT(*) AS users,
  ROUND(EXTRACT(EPOCH FROM PERCENTILE_CONT(0.5)
    WITHIN GROUP (ORDER BY first_value_at - signup_at)) / 3600, 1) AS median_ttv_hours,
  ROUND(EXTRACT(EPOCH FROM PERCENTILE_CONT(0.9)
    WITHIN GROUP (ORDER BY first_value_at - signup_at)) / 3600, 1) AS p90_ttv_hours
FROM first_value
WHERE first_value_at IS NOT NULL
GROUP BY cohort_week
ORDER BY cohort_week;

Связь с удержанием

Чем короче TTV, тем выше удержание. Это можно проверить SQL'ем:

WITH ttv AS (
  SELECT
    u.user_id,
    EXTRACT(EPOCH FROM (MIN(e.created_at) - u.signup_at)) / 3600 AS ttv_hours
  FROM users u
  JOIN events e
    ON e.user_id = u.user_id
    AND e.event_type = 'first_order_completed'
  GROUP BY u.user_id, u.signup_at
),
ttv_buckets AS (
  SELECT
    user_id,
    CASE
      WHEN ttv_hours <= 1 THEN '<1 hour'
      WHEN ttv_hours <= 24 THEN '1-24 hours'
      WHEN ttv_hours <= 168 THEN '1-7 days'
      ELSE '>7 days'
    END AS ttv_bucket
  FROM ttv
),
d30_retention AS (
  SELECT
    user_id,
    EXISTS (
      SELECT 1 FROM events
      WHERE user_id = user_id
        AND created_at >= NOW() - INTERVAL '30 days'
    ) AS retained_d30
  FROM users
)
SELECT
  tb.ttv_bucket,
  COUNT(*) AS users,
  ROUND(AVG(CASE WHEN dr.retained_d30 THEN 1.0 ELSE 0 END), 3) AS d30_retention
FROM ttv_buckets tb
JOIN d30_retention dr USING (user_id)
GROUP BY tb.ttv_bucket
ORDER BY tb.ttv_bucket;

Ожидаем: чем меньше TTV, тем выше удержание на D30. Если связь обратная — value-событие выбрано неправильно.

Частые ошибки

Среднее вместо медианы. TTV имеет длинный хвост. Один пользователь с TTV 30 дней тянет среднее вверх. Используйте медиану.

Игнорировать тех, кто не достиг ценности. Если 70% не активировались, медиана TTV считается только по оставшимся 30%. Это смещение. Считайте отдельно медиану TTV и долю тех, кто дошёл до ценности.

Использовать часовой пояс сервера. Пользователь из UTC+3 регистрируется в 23:00 UTC, а value-событие совершает в 02:00 UTC — TTV считается «3 часа», но фактически было 23:30 локального → 5:30 локального = 6 часов. Используйте локальное время.

Не учитывать новые когорты. Пользователь, зарегистрировавшийся 2 дня назад, не имел шанса дойти до ценности за 7 дней. Считайте только когорты, у которых окно уже прошло.

Брать «слабое» value-событие. «Открыл приложение второй раз» — слабая ценность: она не предсказывает удержание. Берите событие, которое реально отражает ключевую ценность продукта.

FAQ

Какой TTV считается хорошим?

Всё зависит от продукта: для соцсети хороший TTV измеряется минутами, для маркетплейса доставки — часами, для B2B-SaaS — днями. Универсального числа нет, поэтому ориентируйтесь на свою категорию и старайтесь снижать TTV относительно собственной базовой линии.

Как использовать TTV в работе?

TTV удобно двигать через A/B-тесты онбординга. Если новый онбординг сократил медианный TTV с 24 до 6 часов — это сильный сигнал, что вырастет и удержание. Поэтому TTV часто используют как быстрый прокси-показатель качества активации.

Это официальная информация?

Нет, это не официальный стандарт. Статья основана на общепринятых отраслевых практиках.