SQL-проверки качества данных: дубли, пропуски и скачки метрик
Практический чеклист SQL-проверок перед дашбордом: найти дубли, пропущенные ключи, события без пользователей и неожиданные скачки дневного объёма.

Самый опасный аналитический отчёт — не пустой, а уверенный и неправильный. Дашборд может показать падение DAU из-за задержки загрузки, рост выручки из-за дублей платежей или хороший retention из-за неверной даты старта. Несколько коротких SQL-проверок до расчёта помогают отделить продуктовый сигнал от поломки данных.
Коротко
Проверка качества должна отвечать на четыре вопроса: есть ли дубли, хватает ли ключевых значений, согласованы ли связи между таблицами и не изменился ли объём данных аномально. Это не отдельный большой проект: базовый набор можно запускать перед каждым важным отчётом.
- Проверь размер таблицы и период загрузки.
- Найди NULL в ключевых полях и неожиданные пустые значения.
- Проверь дубли на уровне бизнес-события.
- Найди события без пользователя и платежи без связанной сущности.
- Сравни дневной объём с предыдущими днями и ожидаемым диапазоном.
Сначала зафиксируй контракт данных
SQL-проверка имеет смысл только относительно ожидаемого правила. Для `users` одна строка — один пользователь, `user_id` уникален и заполнен. Для `events` одна строка — одно событие, user_id может быть анонимным только по согласованному правилу. Для payments сумма не должна быть отрицательной без отдельного типа возврата.
| Таблица | Ожидаемое правило | Что проверяем |
|---|---|---|
| users | один user_id на строку | дубли и пропущенный ключ |
| events | одно событие на строку | дубликаты, дата и orphan user_id |
| payments | одна операция на строку | сумма, дата и связь с user_id |
Проверка NULL и обязательных полей
Начни с полей, без которых метрика меняет смысл: user_id, event_date, event_name, amount. Не каждый NULL является ошибкой, но каждый должен иметь объяснение. Если user_id может быть анонимным, это правило нужно отразить в расчёте DAU, а не скрывать фильтром.
select\n count(*) as all_events,\n count(*) filter (where user_id is null) as missing_user_id,\n count(*) filter (where event_date is null) as missing_event_date,\n count(*) filter (where event_name is null) as missing_event_name\nfrom events;Условие user_id is not null может быть частью метрики, но отдельно сохраняй число отброшенных строк. Иначе качество трекинга ухудшается, а отчёт просто продолжает выглядеть аккуратно.
Дубли: одна строка или повтор события
Дубликат нужно определять по бизнес-ключу, а не только по полному совпадению строки. Для события это может быть event_id, либо комбинация user_id, event_date, event_name и session_id. Для payments лучше иметь отдельный payment_id. Если ключа нет, сначала договорись, что именно считается повтором.
| Результат | Возможное объяснение | Следующее действие |
|---|---|---|
| Повторы в один день | пользователь повторил действие | проверить session_id или event_id |
| Идентичные строки | повторная загрузка batch | сверить ingestion job и ключ дедупликации |
| Скачок после релиза | изменился трекинг | сравнить схему событий до и после релиза |
select\n user_id,\n event_date,\n event_name,\n count(*) as rows_count\nfrom events\ngroup by user_id, event_date, event_name\nhaving count(*) > 1\norder by rows_count desc;Orphan rows: события без пользователя
Событие с user_id, которого нет в users, может быть нормальным для ещё не загруженного справочника, а может означать сломанную связь или неверный идентификатор. LEFT JOIN помогает найти такие строки, не теряя события из исходной таблицы.
select\n e.user_id,\n count(*) as events_count\nfrom events as e\nleft join users as u using (user_id)\nwhere u.user_id is null\ngroup by e.user_id\norder by events_count desc;- Проверь, не различаются ли типы user_id: integer и text.
- Сверь задержку загрузки users и events.
- Отдельно обработай anonymous_id, если он есть в продукте.
- Не удаляй orphan rows до понимания причины.
Скачки дневного объёма
Аномальный объём событий не всегда означает рост активности. Причиной может быть дубль загрузки, новый SDK, изменение названия события или задержка нескольких дней, которая пришла одной пачкой. Сначала агрегируй объём по дню, затем сравни с предыдущим днём через LAG.
with daily as (\n select\n event_date,\n count(*) as events_count\n from events\n group by event_date\n), daily_with_previous as (\n select\n event_date,\n events_count,\n lag(events_count) over (order by event_date) as previous_count\n from daily\n)\nselect\n event_date,\n events_count,\n previous_count,\n events_count - previous_count as change\nfrom daily_with_previous\norder by event_date;Порог вроде “в два раза больше вчера” помогает найти точку внимания, но не доказывает баг. Сверь релизы, кампании, источники событий и время загрузки.
Сверка метрики с независимым расчётом
Если дашборд показывает DAU, полезно иметь простой контрольный запрос с тем же определением active action. Сверяй не только итоговое число, но и период, таймзону, фильтры тестовых аккаунтов и количество уникальных пользователей.
| Параметр | Пример |
|---|---|
| Active action | core_action |
| Период | 2026-06-01 ≤ day < 2026-07-01 |
| Идентичность | distinct user_id, anonymous отдельно |
| Исключения | QA, сотрудники, боты |
select\n event_date,\n count(distinct user_id) as dau\nfrom events\nwhere event_name = 'core_action'\n and user_id is not null\n and event_date >= date '2026-06-01'\n and event_date < date '2026-07-01'\ngroup by event_date\norder by event_date;Чеклист перед публикацией отчёта
Проверка качества не должна превращаться в ритуал из десятков непонятных запросов. Начни с пяти вопросов, сохрани результат и добавь ссылку на него к дашборду или задаче аналитика.
- Размер таблицы и свежесть последней даты ожидаемые?
- Ключи заполнены и не дублируются?
- Связи между таблицами не дают неожиданное число orphan rows?
- Дневной объём и доля событий по типам не скачут без объяснения?
- Контрольный расчёт метрики совпадает с дашбордом по определению?
Мини-практика и следующий шаг
Собери мини-аудит таблицы events: пропуски, дубли, orphan user_id и дневной объём. Затем сравни контрольный DAU с результатом из отчёта. В конце запиши, какие проверки можно запускать автоматически перед обновлением дашборда.
Материалы по теме
SQL даты и время в аналитике: периоды, недели и возраст пользователя
Как работать с датами в SQL: строить полные периоды, группировать события по дням и неделям, считать возраст пользователя и не ошибаться на границах интервалов.
Читать материал
SQL CASE, COALESCE и NULL: как не сломать сегменты и метрики
Разбираем NULL, CASE и COALESCE на задачах аналитика: как группировать пустые значения, создавать сегменты и показывать ноль вместо пропущенных данных.
Читать материал
Когорты и retention в SQL: как считать возвращаемость по датам
Разбираем когортный retention в SQL: как определить дату старта, посчитать D1 и D7, собрать матрицу возвращаемости и сравнить каналы без самообмана.
Читать материал