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?
- Дневной объём и доля событий по типам не скачут без объяснения?
- Контрольный расчёт метрики совпадает с дашбордом по определению?
Слой quality_checks для регулярного отчёта
Разовые запросы полезны для расследования, но регулярному отчёту нужен отдельный слой проверок. Собери результат в таблицу или view, где одна строка — одна проверка за один запуск: имя проверки, дата среза, фактическое значение, ожидаемый диапазон и статус. Тогда дашборд может показать не только метрику, но и состояние входных данных.
Порог должен быть связан с процессом. Для свежести источника можно проверить, что последняя дата не старше двух часов. Для доли NULL допустим диапазон до 1%, а для уникальности user_id — ноль повторов. Не копируй пороги из чужого проекта: они зависят от частоты загрузки, размера продукта и стоимости ошибки.
Если проверка красная, отчёт не обязан исчезать. Иногда безопаснее показать последний подтверждённый срез с явной пометкой, чем заменить его неполными данными. Это решение должно быть заранее согласовано с владельцем дашборда, а не приниматься в момент инцидента.
select
check_name,
snapshot_date,
actual_value,
expected_min,
expected_max,
case
when actual_value between expected_min and expected_max
then 'ok'
else 'alert'
end as status
from quality_check_results
where snapshot_date = current_date;Как отличить поломку данных от продуктового события
Скачок объёма или падение DAU требует двух параллельных проверок. Сначала проверь pipeline: freshness, дубли, долю NULL и распределение event_name. Затем посмотри на релизы, маркетинговые кампании и реальные изменения поведения. Нельзя объявлять багом любой необычный график, но нельзя и списывать поломку трекинга на активность пользователей.
Полезно сравнивать несколько независимых сигналов. Если DAU упал, но число сессий, оплат и серверных запросов осталось стабильным, проблема может быть в событии активности. Если все сигналы изменились одновременно после релиза, вероятность продуктового события выше. Данные не дают готовую причинность, но помогают правильно поставить расследование.
В итоговом отчёте пиши не только статус «данные плохие». Укажи, какая проверка сработала, какой диапазон ожидался, сколько строк затронуто и кто должен подтвердить следующий шаг. Так quality-check становится рабочим инструментом, а не набором тревожных красных ячеек.
- Проверять свежесть до расчёта метрики.
- Сохранять фактическое значение и ожидаемый диапазон.
- Сверять продуктовый сигнал с независимыми таблицами.
- Не скрывать отфильтрованные строки.
- Заранее определить поведение отчёта при красной проверке.
Мини-практика и следующий шаг
Собери мини-аудит таблицы events: пропуски, дубли, orphan user_id и дневной объём. Затем сравни контрольный DAU с результатом из отчёта. В конце запиши, какие проверки можно запускать автоматически перед обновлением дашборда.
Материалы по теме

SQL CASE, COALESCE и NULL: как не сломать сегменты и метрики
Разбираем NULL, CASE и COALESCE на задачах аналитика: как группировать пустые значения, создавать сегменты и показывать ноль вместо пропущенных данных.

SQL JOIN для аналитика: как соединять users, events и payments
Понятное объяснение INNER JOIN и LEFT JOIN: как связать пользователей с событиями и платежами, не потерять сегменты и не завысить метрику после соединения.
Собеседование BI-аналитика: дашборды, Excel, SQL и бизнес-кейсы
Практический гайд по собеседованию BI-аналитика: как проектировать дашборд, выбирать KPI, проверять данные, отвечать про Excel и защищать вывод перед бизнесом.