Учебная база данных для SQL: пример базы продукта на 45 185 строк
Учебная база данных для практики SQL: пять таблиц SaaS-продукта, связи, словарь колонок, первые запросы с результатами, A/B-тест внутри и честный список того, чего в ней нет.
Содержание статьи
Большинство учебных баз для SQL описывают магазин: заказы импортёра деликатесов в Northwind, прокат DVD в Sakila, производителя велосипедов в AdventureWorks. На них хорошо учить JOIN и GROUP BY, но вопросы, которые задают продуктовому аналитику, там не задать: нет событий в продукте, нет подписок и нет эксперимента. Ниже описана другая база — учебная база SQL-курса КейсПрактики. Это SaaS-сервис: пять таблиц, 45 185 строк, 90 дней регистраций, платежи с продлениями и A/B-тест, в котором средний результат скрывает сегмент. На ней посчитаны примеры новых SQL-статей блога, и любой запрос отсюда можно выполнить в песочнице курса без установки и регистрации. Статья — путеводитель: что лежит в каждой таблице, как таблицы связаны, какие вопросы им можно задать и чего в базе нет.
Коротко
База описывает один продукт за один квартал. Люди регистрируются, открывают приложение, создают рабочее пространство и отчёты, приглашают коллег и платят за тариф. Всё это разложено по пяти таблицам, которые связаны через user_id.
users— 4 613 регистраций с 1 июня по 29 августа 2026 года: канал, страна, устройство.events— 35 341 событие пяти типов. Выгрузка закрыта 30 августа в 13:00, поэтому последний день неполный.payments— 1 251 платёж от 912 человек на $30 639: первые оплаты и продления.subscriptions— 912 подписок, ровно одна на плательщика, со статусом active, past_due или churned.experiment_exposures— 3 068 участников теста чек-листа онбординга: +10 п. п. конверсии в среднем и ноль на мобильных.- Запросы выполняются в песочнице курса на DuckDB — том же движке, которым курс проверяет задания. Числа из этой статьи совпадают с песочницей до единицы.
Что внутри: пять таблиц и как они связаны
Схема устроена звездой вокруг users. У каждой из четырёх других таблиц есть колонка user_id, которая указывает на пользователя. Это внешний ключ: по нему платёж, событие или подписка находят своего владельца. Собственный идентификатор строки есть у каждой таблицы — event_id, payment_id и так далее.
Перед первым запросом к любой таблице стоит назвать её зерно — что описывает одна строка. У users одна строка — человек, у payments — платёж, и один человек может занимать в ней три строки. Почти все ошибки новичка в JOIN и COUNT происходят из-за того, что зерно двух таблиц не совпало, а запрос этого не заметил.
Ключи в базе чистые: у всех 35 341 события, 1 251 платежа, 912 подписок и 3 068 участников эксперимента есть пользователь в users, «сирот» ноль. Ограничения PRIMARY KEY и FOREIGN KEY при этом не объявлены — таблицы собраны через CREATE TABLE AS, как это часто бывает в аналитических хранилищах. Как проверить ключи самому, разобрано в статье про первичный и внешний ключ.
| Таблица | Одна строка | Строк | Ключ строки | Связь с users |
|---|---|---|---|---|
| users | зарегистрированный пользователь | 4 613 | user_id | — |
| events | действие в продукте | 35 341 | event_id | много событий у одного человека |
| payments | успешный платёж | 1 251 | payment_id | от одного до трёх платежей у плательщика |
| subscriptions | подписка плательщика | 912 | subscription_id | не больше одной на человека |
| experiment_exposures | попадание в вариант эксперимента | 3 068 | exposure_id | не больше одного на человека |
SELECT 'users' AS table_name, count(*) AS row_count FROM users
UNION ALL SELECT 'events', count(*) FROM events
UNION ALL SELECT 'payments', count(*) FROM payments
UNION ALL SELECT 'subscriptions', count(*) FROM subscriptions
UNION ALL SELECT 'experiment_exposures', count(*) FROM experiment_exposures;
-- 4 613, 35 341, 1 251, 912, 3 068 — всего 45 185 строкКак выглядит один пользователь во всех пяти таблицах
Схему проще понять на одном человеке. Пользователь 8 зарегистрировался 1 июня из органики, с десктопа, из России. В тот же день в 11:56 он открыл приложение, через 35 минут создал рабочее пространство, в 13:56 — первый отчёт. На следующий день пригласил коллегу, через день выгрузил отчёт. Потом возвращался восемь раз, последний — 20 июля.
Платил он трижды: 10 июня, 10 июля и 9 августа, каждый раз $29 за тариф pro. Первая оплата помечена как first, две следующие — как renewal. В subscriptions у него одна строка со статусом active, а в эксперименте он попал в контрольную группу и дошёл до целевого действия.
Итого одна строка в users превращается в 13 строк в events и 3 в payments. Если соединить эти две таблицы по user_id, получится 13 × 3 = 39 строк на одного человека, и сумма его платежей в таком результате вырастет в 13 раз. Это главный риск работы с базой, и он виден уже на одном пользователе.
| Таблица | Строк | Что в них |
|---|---|---|
| users | 1 | 2026-06-01, organic, RU, desktop |
| events | 13 | app_open ×9, workspace_created, report_created, invite_sent, export_completed |
| payments | 3 | pro по $29: first 10 июня, renewal 10 июля и 9 августа |
| subscriptions | 1 | pro, active, с 10 июня |
| experiment_exposures | 1 | onboarding_checklist, control, converted = true |
SELECT 'users' AS table_name, count(*) AS row_count FROM users WHERE user_id = 8
UNION ALL SELECT 'events', count(*) FROM events WHERE user_id = 8
UNION ALL SELECT 'payments', count(*) FROM payments WHERE user_id = 8
UNION ALL SELECT 'subscriptions', count(*) FROM subscriptions WHERE user_id = 8
UNION ALL SELECT 'experiment_exposures', count(*) FROM experiment_exposures WHERE user_id = 8;
-- 1, 13, 3, 1, 1users: 4 613 регистраций за 90 дней
Колонок пять: user_id (INTEGER), signup_date (DATE), channel, country и device (все VARCHAR). Регистрации идут каждый день с 1 июня по 29 августа, 90 дней подряд. Поток растёт: в первую неделю июня 196 регистраций, в неделю с 17 августа — 481.
В данных есть три особенности, на которых строятся задания. Будни сильнее выходных: в будний день в среднем 61 регистрация, в выходной — 25. Сервисом пользуются на работе, и регистрируются в нём тоже с рабочего места. 15 и 16 июля идёт кампания — 99 и 100 регистраций, это два самых больших дня квартала. Канал paid_search появляется только 1 июля: в июне из него нет ни одного человека, и сравнивать каналы «за квартал» без поправки на это нельзя.
Каналы за квартал: organic — 1 747, referral — 1 037, paid_search — 1 015, partner — 814. Страны: RU — 2 715, KZ — 912, AM — 517, BY — 414, и у 55 человек страна не заполнена. Этот NULL ведёт себя как в рабочих данных: count(country) вернёт 4 558, а не 4 613, и фильтр country <> 'RU' этих 55 не покажет. Устройства делятся почти поровну: desktop — 2 384, mobile — 2 229.
| Месяц | organic | referral | partner | paid_search | Всего |
|---|---|---|---|---|---|
| июнь | 478 | 292 | 272 | — | 1 042 |
| июль | 597 | 352 | 245 | 482 | 1 676 |
| август (по 29-е) | 672 | 393 | 297 | 533 | 1 895 |
Неделя начинается с понедельника. Пик недели 13 июля — кампания 15–16 июля. В последней неделе шесть дней: выгрузка регистраций заканчивается субботой 29 августа.
events: 35 341 событие и пять действий в продукте
Колонки: event_id, user_id, event_time (TIMESTAMP) и event_name. Событий пять типов, и у них разное зерно. app_open повторяется: 28 693 открытия у всех 4 613 человек. Остальные четыре случаются не больше одного раза на пользователя — число строк у них равно числу людей. Это удобно для воронки и опасно для JOIN: соединение с app_open размножает строки, соединение с workspace_created — нет.
События идут с 1 июня 08:38 до 30 августа 12:59. У каждого пользователя окно наблюдения — до двух месяцев после регистрации, но не дальше конца выгрузки. Последний день выгрузки неполный: 30 августа — 226 событий, накануне — 588. Если построить DAU по дням и не отрезать этот день, график покажет обвал, которого не было.
Время записано без часового пояса. В описании базы это UTC+3, как в Москве, и граница дня в расчётах проходит по этому времени.
| event_name | Событий | Пользователей | Что означает |
|---|---|---|---|
| app_open | 28 693 | 4 613 | открыл приложение, в том числе при возврате |
| workspace_created | 2 750 | 2 750 | создал рабочее пространство — активация |
| report_created | 1 775 | 1 775 | собрал первый отчёт |
| invite_sent | 1 135 | 1 135 | пригласил коллегу |
| export_completed | 988 | 988 | выгрузил отчёт |
SELECT event_name,
count(*) AS events,
count(DISTINCT user_id) AS users
FROM events
GROUP BY event_name
ORDER BY events DESC;
-- app_open 28 693 / 4 613, workspace_created 2 750 / 2 750, …payments и subscriptions: деньги и статусы
В payments шесть колонок: payment_id, user_id, paid_at (DATE), amount (DOUBLE, в долларах), plan и payment_type. Тарифов три: basic за $19, pro за $29 и team за $39. Первый платёж помечен first, продления — renewal, они идут каждые 30 дней. Всего 1 251 платёж на $30 639: 912 первых на $22 328 и 339 продлений на $8 311.
Из 912 плательщиков 622 заплатили один раз, 241 — дважды, 49 — трижды. Четвёртого платежа нет ни у кого: окно выгрузки закрывается раньше. Отсюда разные «средние» из одной таблицы: средний платёж $24,49, средняя выручка на плательщика $33,60, выручка на зарегистрированного пользователя $6,64.
В subscriptions одна строка на плательщика: 912 подписок у 912 разных людей. started_at совпадает с датой первого платежа, plan — тариф первого платежа, monthly_price — его месячная цена. Статусы: active — 515 подписок на $12 595 в месяц, past_due — 228 на $5 632, churned — 169 на $4 101. Какой MRR показывать, только active или вместе с past_due, — решение, которое аналитик обязан проговорить, и база даёт его принять на числах.
| plan | Цена | Платежей | Плательщиков | Выручка |
|---|---|---|---|---|
| basic | $19 | 706 | 514 | $13 414 |
| pro | $29 | 403 | 296 | $11 687 |
| team | $39 | 142 | 102 | $5 538 |
| всего | 1 251 | 912 | $30 639 |
experiment_exposures: A/B-тест, который можно разобрать до конца
Таблица описывает один эксперимент — onboarding_checklist: новым пользователям показывали чек-лист первых шагов. В тест попали 3 068 человек из 4 613: 1 527 в контрольную группу и 1 541 в вариант с чек-листом. Колонка converted (BOOLEAN) — дошёл ли участник до целевого действия.
В среднем чек-лист выигрывает: 30,3% против 20,0%. Разрез по устройству из users меняет вывод. На десктопе конверсия выросла с 20,0% до 39,8%, на мобильных осталась прежней — 20,1% против 19,9%. Весь прирост дал десктоп, и решение «раскатить на всех» опиралось бы на среднее, которое не описывает ни одну из двух групп.
Этот сюжет можно пройти до конца на одной базе: проверить, что группы разделились поровну, посчитать конверсию, разложить её по сегментам, проверить значимость и сформулировать решение. Поэтому таблица и лежит в базе целиком, а не готовым итогом.
Контроль: 789 человек на десктопе и 738 на мобильных. Чек-лист: 807 и 734. Посчитано на учебной базе SQL-курса.
SELECT u.device,
x.variant,
count(*) AS participants,
round(100.0 * avg(x.converted::INT), 1) AS conversion_pct
FROM experiment_exposures x
JOIN users u ON u.user_id = x.user_id
GROUP BY u.device, x.variant
ORDER BY u.device, x.variant;
-- desktop: checklist 39.8, control 20.0
-- mobile: checklist 19.9, control 20.1С каких запросов начать
Первый запрос к незнакомой базе — всегда SELECT * FROM таблица LIMIT 5 для каждой таблицы: посмотреть колонки, типы и то, как выглядят значения. Второй — подсчёт строк и различных ключей, как в разделе выше. Третий — первый вопрос, ради которого база нужна.
Хороший третий запрос здесь — активация по каналам: какая доля зарегистрировавшихся создала рабочее пространство. Он требует соединить две таблицы, выбрать условие соединения и не потерять людей без события. Ответ сразу даёт продуктовый вывод: referral активируется в 74,7% случаев, paid_search — в 37,0%, вдвое хуже.
Обратите внимание, где стоит условие на тип события. Если перенести его из ON в WHERE, LEFT JOIN превратится во внутреннее соединение: люди без workspace_created выпадут из знаменателя, и активация у всех каналов станет 100%.
SELECT u.channel,
count(*) AS users,
count(e.user_id) AS activated,
round(100.0 * count(e.user_id) / count(*), 1) AS activation_pct
FROM users u
LEFT JOIN events e
ON e.user_id = u.user_id
AND e.event_name = 'workspace_created'
GROUP BY u.channel
ORDER BY activation_pct DESC;
-- referral 74.7, organic 66.7, partner 53.2, paid_search 37.0Какие вопросы можно задать этой базе
Ниже — карта: рабочий вопрос, таблицы, которые для него нужны, и где он разобран подробно. Главы курса с пометкой «полная база» работают на тех же 45 185 строках, что и песочница. Остальные главы учат синтаксису на срезе того же продукта — 18 пользователей первой недели июня, которых видно целиком и можно пересчитать руками.
| Вопрос | Таблицы | Статья | Глава курса |
|---|---|---|---|
| сколько людей пользуются продуктом каждый день | events | DAU | 6, полная база |
| какая доля новых дошла до ценности | users, events | activation rate | 7, полная база |
| где пользователи теряются по пути | events | воронка в SQL | 8, полная база |
| возвращаются ли на следующий день | users, events | когорты и retention | 9 и 16, полная база |
| что приносит paid_search от регистрации до выручки | users, events, payments | ARPU и ARPPU | 13, полная база |
| сколько людей платят и сколько приносят | users, payments | JOIN без дублей | 3–4 |
| сработал ли эксперимент | experiment_exposures, users | A/B-тест в SQL | 17 |
| можно ли доверять цифрам | все | проверки качества данных | 14–15 |
Чем она отличается от Northwind, Sakila и AdventureWorks
Классические учебные базы делали для разработчиков и администраторов, и это видно по схемам: много нормализованных справочников, заказы, склад, сотрудники. На них хорошо тренировать соединение пяти таблиц подряд. Хуже — вопросы про поведение во времени: у заказа есть дата, но нет пути пользователя до него.
Учебная база курса устроена наоборот: справочников нет, таблиц мало, а время и поведение — главное. Поэтому на ней неудобно учить длинные цепочки JOIN, зато удобно учить метрики продукта, окна наблюдения и сегменты.
| База | Что описывает | Сильная сторона | Чего нет для продуктового аналитика |
|---|---|---|---|
| Northwind | торговец деликатесами: заказы, поставщики, сотрудники | классический JOIN по многим таблицам | событий, подписок, экспериментов |
| Sakila (и Pagila для PostgreSQL) | прокат DVD: фильмы, копии, аренды, платежи | связи многие-ко-многим, несколько путей между таблицами | пути пользователя в продукте |
| AdventureWorks | производитель велосипедов | большая корпоративная схема, продажи и производство | поведенческих данных |
| Chinook | цифровой музыкальный магазин | компактная схема, есть для многих СУБД | событий и экспериментов |
| базы sql-ex | компьютерная фирма, корабли, авиарейсы | сотни упражнений с проверкой | продуктовых метрик |
| учебная база SQL-курса | SaaS-сервис: регистрации, события, платежи, подписки, A/B | метрики, когорты, окна, сегменты | справочников и длинных цепочек JOIN |
Где выполнить запросы без установки
В песочнице SQL-курса. Она открыта без регистрации, в ней уже загружены все пять таблиц, рядом — схема с колонками и типами. Запрос выполняется на сервере в DuckDB — том же движке и на той же базе, что проверяют задания курса. Поэтому числа из статей, посчитанных на учебной базе, совпадают с тем, что вы увидите.
Песочница работает только на чтение. Разрешён один запрос за запуск, и он должен начинаться с SELECT или WITH. Создать таблицу, вставить строки или удалить их нельзя: база общая, и каждый запуск начинается с одинакового состояния. Промежуточные расчёты пишите через CTE.
Диалект DuckDB близок к PostgreSQL: те же date_trunc, ::INT, FILTER, оконные функции. Отличия есть, и они мелкие. Например, DuckDB разрешает сослаться на псевдоним колонки из SELECT прямо в WHERE, а PostgreSQL на тот же запрос ответит column "doubled" does not exist. Если переносите запрос в рабочую базу, такие места стоит проверить.
Ограничения: чего в базе нет
Данные синтетические. Они сгенерированы детерминированно, без случайных чисел, поэтому база одинакова при каждой пересборке и эталонные ответы не разъезжаются. При этом у них есть форма, как у живого продукта: рост, недельная сезонность, запуск канала посреди периода, затухающий возврат и неполный последний день. Выводы о реальных SaaS-продуктах из этих чисел делать нельзя, учиться на них — можно.
- Нет затрат на привлечение. CAC и окупаемость канала не посчитать: денег на рекламу в базе нет.
- Нет сессий, страниц и свойств событий. Событие — это имя и время, без параметров и JSON.
- Нет возвратов, неудачных списаний и смены тарифа. Статус подписки — готовое поле, и past_due здесь зависит от активности человека, а не от просроченного платежа.
- Флаг
convertedв эксперименте хранится готовым. Изeventsего не восстановить: целевое действие в базе не расшифровано. - Время попадания в эксперимент условное — 00:05 в день регистрации, раньше первого открытия. Проверить порядок «попал в тест → совершил действие» по времени не получится.
- Когорта 29 августа не дозрела: у неё нет полного следующего дня. Для D1 и более длинных окон её нужно отбрасывать.
- Ограничения PRIMARY KEY и FOREIGN KEY не объявлены. Ключи чистые, но это проверяется запросом, а не гарантируется базой.
Частые вопросы
Нужно ли ставить PostgreSQL, чтобы повторить примеры? Нет. Запрос пишется в браузере, а выполняет его сервер песочницы. Ставить базу, импортировать дамп и настраивать подключение не нужно.
Почему в статьях доллары, а не рубли? Так устроен продукт в базе: тарифы basic, pro и team стоят $19, $29 и $39 в месяц. Для задач это ничего не меняет.
Подходит ли база для подготовки к собеседованию? Да, для той части, где просят посчитать метрику: DAU, активацию, воронку, retention, ARPPU, конверсию в A/B-тесте. Задачи на длинные цепочки JOIN по справочникам лучше тренировать на Northwind или Sakila.
Можно ли в песочнице создать свою таблицу? Нет, она только на чтение. Промежуточную таблицу заменяет CTE: WITH per_user AS (…) SELECT … FROM per_user.
Итог
Учебная база SQL-курса — это один продукт, на котором можно пройти путь аналитика от SELECT * до решения по эксперименту. Пять таблиц, 45 185 строк, связи через user_id и несколько встроенных ловушек: неполный последний день, канал, появившийся в середине периода, NULL в стране, повторяющиеся платежи и средний uplift, который держится на одном сегменте.
Начните с подсчёта строк и одного пользователя во всех таблицах, затем повторите активацию по каналам. Дальше — задачи с решениями на этой же базе и главы курса. Первые главы — устройство базы, SELECT и WHERE — открыты бесплатно.
Материалы по теме
Ошибки в SQL-запросах: тексты сообщений, причины и исправления
Частые ошибки SQL с дословными текстами PostgreSQL и DuckDB: GROUP BY, column does not exist, ambiguous, division by zero, типы и даты, и пять запросов, которые молча врут.
CASE WHEN в SQL: сегменты, условные метрики и порядок условий
CASE WHEN в SQL на учебной базе продукта: синтаксис, сегменты пользователей, почему порядок WHEN меняет ответ, ELSE и NULL, CASE внутри COUNT, в GROUP BY и ORDER BY.
Что такое SQL простыми словами: реляционная база, таблицы и запросы
Что такое SQL и реляционная база данных: таблица, строка и колонка, первый запрос с результатом, JOIN, команды языка, СУБД и диалекты, что SQL умеет и чего не умеет.