- Превращать формулы среднего чека, удержания и конверсии в SQL-запросы
- Строить когортную таблицу и воронку и интерпретировать результаты с учётом статистических ограничений
- Вычислять медиану, перцентили и ABC-анализ с помощью оконных функций
Владелец магазина задаёт аналитику четыре вопроса: «Сколько в среднем стоит один заказ? Возвращаются ли покупатели? На каком этапе мы их теряем? Какие товары приносят большую часть выручки?». Ответы уже лежат в базе — их нужно лишь извлечь правильным запросом. В этом уроке мы объединим всё, что было в курсе, — JOIN, GROUP BY, CTE, CASE, оконные функции — в настоящих аналитических задачах.
Ключевая метрика: средний чек
- AOVсредний чек (average order value), манат
- Rвыручка за период: Σ количество · цена, манат
- Nₒчисло заказов за тот же период
SELECT COUNT(*) AS orders,
ROUND(SUM(o.quantity * p.price), 2) AS revenue,
ROUND(SUM(o.quantity * p.price) / COUNT(*), 2) AS aov,
COUNT(DISTINCT o.customer_id) AS buyers
FROM orders AS o
JOIN products AS p ON p.id = o.product_id;▸ Ожидаемый результат
orders | revenue | aov | buyers 12 | 3942.06 | 328.5 | 7
Когорты и удержание
Когорта — группа пользователей, переживших одно и то же событие в один и тот же период, например, сделавших первый заказ в феврале 2025 года. Если следить за покупателями по когортам, а не всеми вместе, поведение новых покупателей не смешивается с поведением старых. Главный показатель — удержание (retention): доля когорты, которая снова активна в последующие периоды.
- rпроцент удержания когорты
- C₀размер когорты — покупатели, пришедшие в первом периоде
- Aпокупатели, снова сделавшие заказ в последующие периоды
WITH firsts AS (
SELECT customer_id, SUBSTR(MIN(order_date), 1, 7) AS cohort
FROM orders
GROUP BY customer_id
),
flags AS (
SELECT f.customer_id, f.cohort,
EXISTS (SELECT 1 FROM orders AS o
WHERE o.customer_id = f.customer_id
AND SUBSTR(o.order_date, 1, 7) > f.cohort) AS returned
FROM firsts AS f
)
SELECT cohort,
COUNT(*) AS customers,
SUM(returned) AS returned,
ROUND(100.0 * AVG(returned), 1) AS retention_pct
FROM flags
GROUP BY cohort
ORDER BY cohort;▸ Ожидаемый результат
cohort | customers | returned | retention_pct 2025-01 | 1 | 1 | 100 2025-02 | 2 | 2 | 100 2025-03 | 2 | 0 | 0 2025-04 | 1 | 0 | 0 2025-06 | 1 | 0 | 0
EXISTS возвращает 1 или 0, поэтому SUM считает вернувшихся, а AVG даёт их долю.По результату выше вычисли общий процент удержания. Насколько обоснован вывод «мартовская когорта плохая, а январская и февральская — отличные»?
Показать решениеСкрыть решение
2) r = 3 ÷ 7 · 100% ≈ 42,9%.
3) В когортах по 1–2 человека: один покупатель меняет процент от 0 до 100, статистический вывод сделать нельзя.
4) У поздних когорт было меньше времени, чтобы вернуться (за июньской следит только июль), — это «цензурированные» данные. Для честного сравнения нужно смотреть на одинаковое окно для каждой когорты (например, 60 дней после первого заказа).
Воронка и конверсия
Воронка показывает последовательные этапы, которые проходит покупатель; на каждом этапе часть людей «отваливается». В учебной базе нет журнала событий, поэтому строим воронку лояльности: регистрация → не меньше 2 товаров → не меньше 2 заказов → потрачено больше 900 манатов. FIRST_VALUE считает отношение к началу, а LAG — к предыдущему этапу.
- cᵢконверсия до i-го этапа
- nᵢпокупатели, дошедшие до i-го этапа
- n₁первый этап воронки
Пошаговая конверсия — nᵢ ÷ nᵢ₋₁ · 100%; она отвечает на вопрос «где мы теряем больше всего?».
WITH per_customer AS (
SELECT c.id,
COUNT(o.id) AS orders,
COALESCE(SUM(o.quantity), 0) AS items,
COALESCE(SUM(o.quantity * p.price), 0) AS spent
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
LEFT JOIN products AS p ON p.id = o.product_id
GROUP BY c.id
),
funnel AS (
SELECT 1 AS step, 'registered' AS stage, COUNT(*) AS customers FROM per_customer
UNION ALL SELECT 2, '2+ items', COUNT(*) FROM per_customer WHERE items >= 2
UNION ALL SELECT 3, '2+ orders', COUNT(*) FROM per_customer WHERE orders >= 2
UNION ALL SELECT 4, 'spent > 900', COUNT(*) FROM per_customer WHERE spent > 900
)
SELECT step, stage, customers,
ROUND(100.0 * customers / FIRST_VALUE(customers) OVER (ORDER BY step), 1) AS pct_of_start,
ROUND(100.0 * customers / LAG(customers) OVER (ORDER BY step), 1) AS pct_of_prev
FROM funnel
ORDER BY step;▸ Ожидаемый результат
step | stage | customers | pct_of_start | pct_of_prev 1 | registered | 7 | 100 | NULL 2 | 2+ items | 6 | 85.7 | 85.7 3 | 2+ orders | 4 | 57.1 | 66.7 4 | spent > 900 | 3 | 42.9 | 75
Медиана и перцентили
Среднее чувствительно к выбросам: один ноутбук за 1450 манатов сильно поднимает среднюю цену. Медиана — середина отсортированного списка, выбросы на неё почти не влияют. В PostgreSQL для этого есть готовая функция percentile_cont, а в SQLite медиану и перцентили считают с помощью оконных функций. Поэтому для скошенных распределений — выручки, цен, длительностей — добавляй в отчёт и медиану.
- kпозиция перцентиля в отсортированном списке (с 1)
- pперцентиль, например 90
- nчисло значений
Метод ближайшего ранга: p-й перцентиль — наименьшее значение, покрывающее не меньше p% значений.
WITH ordered AS (
SELECT score,
ROW_NUMBER() OVER (ORDER BY score) AS rn,
COUNT(*) OVER () AS n
FROM enrollments
)
SELECT AVG(score) AS median_score,
(SELECT AVG(score) FROM enrollments) AS mean_score
FROM ordered
WHERE rn IN ((n + 1) / 2, (n + 2) / 2);▸ Ожидаемый результат
median_score | mean_score 86.5 | 82.8
Найди 90-й перцентиль 20 баллов методом ближайшего ранга. Отсортированные баллы: 58, 64, 68, 70, 72, 75, 77, 79, 81, 85, 88, 88, 90, 91, 92, 93, 94, 95, 97, 99.
Показать решениеСкрыть решение
2) 18-е значение: 95.
3) Проверка: 18 значений не больше 95, а это 90% от 20.
4) В SQL:
rn = (9 * n + 9) / 10 = (180 + 9) / 10 = 18 (целочисленное деление). Функция percentile_disc(0.9) в PostgreSQL тоже возвращает 95.-- PostgreSQL: built-in ordered-set aggregates
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY score) AS median,
percentile_disc(0.9) WITHIN GROUP (ORDER BY score) AS p90
FROM enrollments;median | p90 --------+----- 86.5 | 95 (1 row)
percentile_cont интерполирует между соседними значениями, а percentile_disc возвращает одно из существующих значений.Мини-проект: ABC-анализ товаров
По принципу Парето большую часть выручки обычно приносит небольшая доля товаров. ABC-анализ это измеряет: сортируем товары по выручке по убыванию, считаем нарастающую долю и присваиваем класс: A, если доля предыдущих товаров ещё не достигла 80%, B — если не достигла 95%, остальные — C. Шаги: 1) выручка каждого товара в CTE; 2) нарастающий и общий итог с помощью окон; 3) класс через CASE.
WITH revenue AS (
SELECT p.name, ROUND(SUM(o.quantity * p.price), 2) AS revenue
FROM orders AS o
JOIN products AS p ON p.id = o.product_id
GROUP BY p.id, p.name
),
running AS (
SELECT name, revenue,
SUM(revenue) OVER (ORDER BY revenue DESC, name
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum,
SUM(revenue) OVER () AS total
FROM revenue
)
SELECT name, revenue,
ROUND(100.0 * cum / total, 1) AS cum_pct,
CASE WHEN (cum - revenue) < 0.80 * total THEN 'A'
WHEN (cum - revenue) < 0.95 * total THEN 'B'
ELSE 'C' END AS abc_class
FROM running
ORDER BY revenue DESC, name;▸ Ожидаемый результат
name | revenue | cum_pct | abc_class Smartphone | 1799.98 | 45.7 | A Laptop | 1450 | 82.4 | A Headphones | 361.5 | 91.6 | B Notebook | 96 | 94 | B Desk lamp | 69.98 | 95.8 | B Backpack | 55 | 97.2 | C Chess set | 42 | 98.3 | C Water bottle | 36 | 99.2 | C Pen set | 31.6 | 100 | C
Итог: два из 9 проданных товаров (22%) приносят 82% выручки — классическая картина Парето. Для класса A стоит особо контролировать остатки и поставки, а для класса C можно подумать об упрощении ассортимента или совместных акциях. Помни: на 12 заказах это лишь демонстрация метода; для реального решения нужны данные за месяцы.
Для каждой страны посчитай число заказов (orders), выручку (revenue, 2 знака после запятой) и средний чек (aov = выручка ÷ заказы, 2 знака). Отсортируй по выручке по убыванию.
SELECT c.country
-- orders, revenue, aov
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
JOIN products AS p ON p.id = o.product_id
GROUP BY c.country
ORDER BY c.country;▸ Ожидаемый результат
country | orders | revenue | aov Azerbaijan | 7 | 2843.49 | 406.21 Türkiye | 3 | 973.59 | 324.53 United Kingdom | 1 | 69.98 | 69.98 Russia | 1 | 55 | 55
С помощью оконных функций найди медиану цен товаров (median_price) и выведи рядом среднюю цену, округлённую до 2 знаков (avg_price). Почему медиана настолько меньше среднего?
WITH ordered AS (
SELECT price
-- row number and total count
FROM products
)
SELECT AVG(price) AS median_price
FROM ordered
-- keep only the middle row(s)
;▸ Ожидаемый результат
median_price | avg_price 48.5 | 293.56
Главное
- Метрики начинаются с формул: средний чек = выручка ÷ заказы, удержание = вернувшиеся ÷ когорта, конверсия = nᵢ ÷ n₁.
- Когорту строят по периоду первого события (
MIN(order_date)), а вернувшихся считают черезEXISTSили условную агрегацию. - Воронку собирают из этапов через
UNION ALL, а конверсию считают с помощьюFIRST_VALUEиLAG. - Медиана устойчива к выбросам; в SQLite её считают через
ROW_NUMBERиCOUNT(*) OVER (), в PostgreSQL — черезpercentile_cont. - Всегда показывай абсолютное число рядом с процентом и сравнивай когорты на одинаковом окне наблюдения.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.