Перейти к содержанию
Educora
Университет30 мин22 / 22

Анализ данных с помощью SQL

Считай бизнес-метрики с помощью SQL: средний чек, когорты и удержание, конверсию воронки, медиану и перцентили, а в конце — проект ABC-анализа на учебных данных.

Проверь себя
В этом уроке ты узнаешь
  • Превращать формулы среднего чека, удержания и конверсии в SQL-запросы
  • Строить когортную таблицу и воронку и интерпретировать результаты с учётом статистических ограничений
  • Вычислять медиану, перцентили и ABC-анализ с помощью оконных функций

Владелец магазина задаёт аналитику четыре вопроса: «Сколько в среднем стоит один заказ? Возвращаются ли покупатели? На каком этапе мы их теряем? Какие товары приносят большую часть выручки?». Ответы уже лежат в базе — их нужно лишь извлечь правильным запросом. В этом уроке мы объединим всё, что было в курсе, — JOIN, GROUP BY, CTE, CASE, оконные функции — в настоящих аналитических задачах.

Ключевая метрика: средний чек

AOV = R ÷ Nₒ
где:
  • AOVсредний чек (average order value), манат
  • Rвыручка за период: Σ количество · цена, манат
  • Nₒчисло заказов за тот же период
SQL
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 = A ÷ C₀ · 100%
где:
  • rпроцент удержания когорты
  • C₀размер когорты — покупатели, пришедшие в первом периоде
  • Aпокупатели, снова сделавшие заказ в последующие периоды
SQL
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
В SQLite EXISTS возвращает 1 или 0, поэтому SUM считает вернувшихся, а AVG даёт их долю.
Пример 1: интерпретация когортной таблицы

По результату выше вычисли общий процент удержания. Насколько обоснован вывод «мартовская когорта плохая, а январская и февральская — отличные»?

Показать решение
1) C₀ = 1 + 2 + 2 + 1 + 1 = 7 покупателей, A = 1 + 2 = 3.
2) r = 3 ÷ 7 · 100% ≈ 42,9%.
3) В когортах по 1–2 человека: один покупатель меняет процент от 0 до 100, статистический вывод сделать нельзя.
4) У поздних когорт было меньше времени, чтобы вернуться (за июньской следит только июль), — это «цензурированные» данные. Для честного сравнения нужно смотреть на одинаковое окно для каждой когорты (например, 60 дней после первого заказа).

Воронка и конверсия

Воронка показывает последовательные этапы, которые проходит покупатель; на каждом этапе часть людей «отваливается». В учебной базе нет журнала событий, поэтому строим воронку лояльности: регистрация → не меньше 2 товаров → не меньше 2 заказов → потрачено больше 900 манатов. FIRST_VALUE считает отношение к началу, а LAG — к предыдущему этапу.

cᵢ = nᵢ ÷ n₁ · 100%
где:
  • cᵢконверсия до i-го этапа
  • nᵢпокупатели, дошедшие до i-го этапа
  • n₁первый этап воронки

Пошаговая конверсия — nᵢ ÷ nᵢ₋₁ · 100%; она отвечает на вопрос «где мы теряем больше всего?».

SQL
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
Самая большая потеря — между 2-м и 3-м этапами: стимулировать повторный заказ (купон на скидку, напоминание) может оказаться самым полезным шагом.

Медиана и перцентили

Среднее чувствительно к выбросам: один ноутбук за 1450 манатов сильно поднимает среднюю цену. Медиана — середина отсортированного списка, выбросы на неё почти не влияют. В PostgreSQL для этого есть готовая функция percentile_cont, а в SQLite медиану и перцентили считают с помощью оконных функций. Поэтому для скошенных распределений — выручки, цен, длительностей — добавляй в отчёт и медиану.

k = ⌈ p ÷ 100 · n ⌉
где:
  • kпозиция перцентиля в отсортированном списке (с 1)
  • pперцентиль, например 90
  • nчисло значений

Метод ближайшего ранга: p-й перцентиль — наименьшее значение, покрывающее не меньше p% значений.

SQL
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
Среднее (82,8) ниже медианы (86,5): несколько низких баллов (58, 64, 68) тянут среднее вниз.
Пример 2: 90-й перцентиль

Найди 90-й перцентиль 20 баллов методом ближайшего ранга. Отсортированные баллы: 58, 64, 68, 70, 72, 75, 77, 79, 81, 85, 88, 88, 90, 91, 92, 93, 94, 95, 97, 99.

Показать решение
1) k = ⌈90 ÷ 100 · 20⌉ = ⌈18⌉ = 18.
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.
SQL
-- 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)
PostgreSQL, не запускается. percentile_cont интерполирует между соседними значениями, а percentile_disc возвращает одно из существующих значений.

Мини-проект: ABC-анализ товаров

По принципу Парето большую часть выручки обычно приносит небольшая доля товаров. ABC-анализ это измеряет: сортируем товары по выручке по убыванию, считаем нарастающую долю и присваиваем класс: A, если доля предыдущих товаров ещё не достигла 80%, B — если не достигла 95%, остальные — C. Шаги: 1) выручка каждого товара в CTE; 2) нарастающий и общий итог с помощью окон; 3) класс через CASE.

SQL
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 знака). Отсортируй по выручке по убыванию.

Задание · SQL
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). Почему медиана настолько меньше среднего?

Задание · SQL
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.

1 / 10
Выручка за период — 3942,06 маната, заказов — 12. Каков средний чек?