← К портфолио SQL Analytics Case Study preview
Ситуация
Нет продакшен-данных, а учебные задачи не показывают системное мышление: 25 кейсов на синтетическом датасете
Задача
Собрать кейсы, где каждый запрос отвечает на продуктовый вопрос и воспроизводится одной командой
Действия
Самодостаточный .sql на кейс, генератор данных на DuckDB, dbt-слой staging → marts, regression-тесты, живой отчёт на Pages
Результат
Воронка теряет 54% на add-to-cart → checkout; retention с ~21% (D1) до ~5% (D30); повторных покупок 3.5%; на real data возвращаются 72.4%
Стек
SQLdbtDuckDBPythonpandas / NumPypytest
Исходники Смотреть на GitHub → Обновлено: Sep 19, 2026
Содержание
  1. Ситуация
  2. Задача
  3. Действия
  4. Запуск
  5. Результат
  6. Ограничения
  7. Документация

SQL Analytics Case Study

Ситуация

Take-home–формат: показать владение SQL на продуктовых задачах, когда продакшен-данных нет, а учебные задачи не демонстрируют системное мышление. 25 end-to-end кейсов на синтетическом продуктовом датасете плюс один real-data кейс на UCI Online Retail II. Каждый кейс — один самодостаточный .sql файл с вопросом и подходом в leading-комментарии. Без сервера, без кредов — одна команда строит данные и базу DuckDB. Дополнительно — dbt-слой (staging → marts, 17 тестов) на той же базе.

Задача

От меня требовалось показать системное мышление, а не знание синтаксиса: собрать кейсы, где каждый запрос отвечает на продуктовый вопрос и воспроизводится одной командой.

Действия

Модель данных (синтетическая, seed=42, детерминированная):

Сущность Объём Поля
Users 20,000 signups (Jan–Jun 2024) channel, country, device, ab_variant
Events ~183k funnel-событий app_open → view_item → add_to_cart → checkout → purchase, ~80k sessions
Orders 928 покупок amount, product category
Subscriptions 268 конверсий monthly / annual plans
Cancellations (seed=43) 98 subscription_cancellations
Refunds (seed=43) 53 refunds
Online Retail II (real) 1,067,371 строк UCI датасет 502, CC BY 4.0 — кейс 26

Схема: data/schema.sql. Генератор: data/generate_data.py. Engagement геометрически убывает от signup; retention взвешен каналом привлечения. Аддитивные таблицы (кейсы 21–25) генерируются на отдельном RNG-потоке (seed=43) — числа кейсов 1–20 не меняются. Real-data таблица (кейс 26) грузится из закоммиченного parquet (data/realdata/) в ту же базу.

25 кейсов:

# Кейс Техника
01 Funnel conversion cumulative counts, LAG / FIRST_VALUE
02 N-day retention by cohort DATE_TRUNC('month', signup_date)
03 Rolling 30-day retention EXISTS subqueries per window
04 DAU / MAU / stickiness trailing-28d range join
05 LTV by cohort left join + COALESCE для zero-revenue
06 Top-N categories per country ROW_NUMBER() OVER (PARTITION BY ...)
07 Cumulative revenue SUM() ... UNBOUNDED PRECEDING
08 Longest active-day streak gaps-and-islands (row_number → island key)
09 A/B conversion by variant in-SQL z-test + p-value (Abramowitz–Stegun)
10 Revenue attribution first-touch vs lifetime, correlated subquery
11 7-day moving average of DAU AVG() OVER (... ROWS BETWEEN 6 PRECEDING ...)
12 Top-2 revenue users per country QUALIFY
13 Monthly revenue by category PIVOT long → wide
14 Subscription MRR recursive CTE (billing rows per subscription)
15 Order amount distribution MEDIAN, QUANTILE_CONT (p90/p99)
16 Sessionization + session depth gaps-and-islands на timestamps, валидация vs ground truth
17 Weekly lifecycle (new/returning/resurrecting/dormant) state transitions, LAG/LEAD
18 Cohort revenue retention (triangle) months-since-signup, % of period-0
19 Repeat purchase & time between orders LAG внутри пользователя
20 RFM segmentation NTILE quintiles, segment-score rules
21 Subscription churn (logo & MRR) monthly churn, FILTER aggregates
22 Refunds & net revenue left join, gross-vs-net
23 Pareto / revenue concentration NTILE(10), cumulative-share curve
24 Daily revenue anomaly detection robust MAD z-score, rolling baseline
25 Purchase → subscription conversion join к subscriptions, time-to-convert
26 Real data — repeat purchase & concentration rollup invoice→customer, order-count buckets, revenue shares

Запуск

uv run python data/generate_data.py   # data/analytics.duckdb (incl. real-data table)
uv run python run.py            # список кейсов
uv run python run.py 1          # запустить кейс 1
uv run python run.py 26         # real-data кейс
uv run --extra dev pytest -q    # 43 regression-теста
uv run python scripts/report.py # reports/index.html
cd dbt && uv run dbt build --profiles-dir .   # dbt: модели + 17 тестов

Runner печатает вопрос кейса, выполняет SQL против data/analytics.duckdb, рендерит результат таблицей. Отчёт с графиками публикуется на GitHub Pages автоматически при push.

Результат

Каждый кейс покрывает конкретный оконно-функциональный паттерн, а находки — честные, а не подогнанные. Три сигнала из teaser (числа сверены с cases.md):

  • воронка теряет 54% на шаге add-to-cart → checkout;
  • retention падает с ~21% (D1) до ~5% (D30) — утечка в onboarding-окне;
  • повторных покупок всего 3.5% (896 покупателей, 31 повторный) — one-and-done purchase engine.

Дополнительно:

  • RFM вырождается в recency-историю; топ-дециль даёт лишь 22% выручки (нет «китов»);
  • лого-churn растёт до ~15%/мес при растущем MRR;
  • sessionization воспроизводит 80k предразмеченных сессий с точностью 99.6%;
  • real data переворачивает вывод: на UCI Online Retail II 72.4% клиентов возвращаются, а топ-15% дают 65% выручки — тот же SQL, противоположный бизнес-вывод.

Расхождение вопрос/подход в одном файле + regression-инварианты делают кейсы самопроверяемыми: 43 pytest-теста и 17 dbt-тестов держат cases.md и код в синхроне, dbt build зелёный в CI.

Ограничения

25 из 26 кейсов — синтетика: их числа описывают форму сгенерированных данных, а не поведение реального продукта. Кейс 26 на UCI Online Retail II показывает, насколько вывод может перевернуться на реальных данных (72.4% repeat против 3.5%), и это честная граница применимости — паттерны SQL переносятся, конкретные метрики нет.

Документация

Аналитика

Данные: github.com/NikitaBoyarkin/sql-analytics-case-study: cases/01_funnel_conversion.sql, cases/13_pivot_revenue.sql, cases/18_cohort_revenue_retention.sql, cases/14_recursive_subscription_mrr.sql — each run as-is against data/analytics.duckdb (deterministic, seed=42, regenerated via data/generate_data.py)

Конверсия воронки покупки (по сессиям)

Число уникальных сессий, дошедших до каждого шага воронки: app_open → просмотр товара → корзина → чекаут → покупка. Данные из cases/01_funnel_conversion.sql по 80 000 сессий синтетического датасета.

app_open: 80000 (100% от макс.) app_open 80000 view_item: 65739 (82.2% от макс.) view_item 65739 add_to_cart: 29641 (37.1% от макс.) add_to_cart 29641 checkout: 6584 (8.2% от макс.) checkout 6584 purchase: 928 (1.2% от макс.) purchase 928
Выводы
  • Сквозная конверсия от открытия приложения до покупки — всего 1.16 % (928 из 80 000 сессий).
  • Крупнейший отвал — корзина → чекаут: 54.91 % корзин не доходят до оформления (29 641 → 6 584).

Выручка по категориям по месяцам

Сумма заказов по категории и месяцу (cases/13_pivot_revenue.sql, PIVOT). Рост через год: суммарная месячная выручка выросла с $2 469.03 в январе до $4 452.48 в июне.

0 500 1000 1500 2024-01 — beauty: 466.84 2024-02 — beauty: 648.97 2024-03 — beauty: 611.16 2024-04 — beauty: 560.09 2024-05 — beauty: 768.52 2024-06 — beauty: 869.45 2024-01 — books: 344.01 2024-02 — books: 664.07 2024-03 — books: 508.99 2024-04 — books: 622.41 2024-05 — books: 532.49 2024-06 — books: 784.77 2024-01 — clothing: 432 2024-02 — clothing: 754.6 2024-03 — clothing: 930.18 2024-04 — clothing: 972.63 2024-05 — clothing: 876.49 2024-06 — clothing: 779.79 2024-01 — electronics: 385.19 2024-02 — electronics: 701.15 2024-03 — electronics: 1134.15 2024-04 — electronics: 981.57 2024-05 — electronics: 946.01 2024-06 — electronics: 656.84 2024-01 — home: 385.64 2024-02 — home: 758.01 2024-03 — home: 727.85 2024-04 — home: 753.31 2024-05 — home: 776.19 2024-06 — home: 717.07 2024-01 — sports: 455.35 2024-02 — sports: 521.8 2024-03 — sports: 919.15 2024-04 — sports: 718.79 2024-05 — sports: 883.96 2024-06 — sports: 644.56 2024-01 2024-02 2024-03 2024-04 2024-05 2024-06 Месяц Выручка, $ beauty books clothing electronics home sports
Выводы
  • Общая выручка выросла в 1.8 раза: с $2 469.03 в январе до $4 452.48 в июне 2024.
  • Пик категории electronics — $1 134.15 в марте, это максимум по всем категориям и месяцам.

Выручка когорт по месяцам с регистрации, % от месяца 0

Выручка каждой когорты регистрации (строки) в месяцы с регистрации M0–M2, в процентах от периода 0 (cases/18_cohort_revenue_retention.sql). Показан полностью наблюдаемый фрагмент 4 × 3.

2024-01 2024-02 2024-03 2024-04 M0 M1 M2 2024-01 · M0: 100% 100% 2024-01 · M1: 85.8% 85.8% 2024-01 · M2: 19.5% 19.5% 2024-02 · M0: 100% 100% 2024-02 · M1: 80.8% 80.8% 2024-02 · M2: 15.3% 15.3% 2024-03 · M0: 100% 100% 2024-03 · M1: 67.3% 67.3% 2024-03 · M2: 12.2% 12.2% 2024-04 · M0: 100% 100% 2024-04 · M1: 87.6% 87.6% 2024-04 · M2: 10.4% 10.4% 0 100%
Выводы
  • Удержание выручки на M1 — 67.3–87.6 %, но на M2 обрушивается до 10.4–19.5 %: выручка «живёт» один месяц после покупки.
  • Слабее всего держит выручку когорта 2024-03: −32.7 п.п. уже на M1 (100 → 67.3 %).

Рост MRR подписок

Месячная повторяющаяся выручка от подписок (cases/14_recursive_subscription_mrr.sql, рекурсивный CTE: monthly = полная сумма, annual = сумма/12). Активные подписки растут с 18 до 268.

0 1000 2000 3000 mrr 2024-01 — mrr: 169.86 2024-02 — mrr: 522.08 2024-03 — mrr: 969.22 2024-04 — mrr: 1411.37 2024-05 — mrr: 1918.45 2024-06 — mrr: 2500.47 2024-01 2024-02 2024-03 2024-04 2024-05 2024-06 Месяц MRR, $
Выводы
  • MRR вырос в 14.7 раза за полгода: с $169.86 в январе до $2 500.47 в июне 2024.
  • Число активных подписок выросло с 18 до 268 — с января по июнь прирост в 14.9 раза.

Карта связей

Проекты, записи и темы, связанные с этим проектом. Наведите на узел, чтобы увидеть название; клик — открыть.