Как найти пропуски в датах SQL

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Зачем это знать

Пропуски в данных = пропуски в отчёте: отсутствующие дни «съедают» seasonality, dashboards показывают неверную динамику. Частая задача в ETL и ad-hoc: «вот daily events, найди дни без событий».

На собесах задача «найти дни, когда не было заказов» — классика middle-уровня. Решается 3 способами, и умение выбрать правильный — важно.

Способ 1: generate_series (Postgres)

SELECT d.day FROM (
    SELECT generate_series('2026-01-01'::DATE, '2026-04-30'::DATE, '1 day') AS day
) d
LEFT JOIN orders o ON DATE(o.created_at) = d.day
WHERE o.id IS NULL;

Полный calendar → LEFT JOIN → NULLs = missing days.

Способ 2: calendar table

Если часто нужно — завести справочную таблицу calendar:

SELECT c.day FROM calendar c
LEFT JOIN orders o ON DATE(o.created_at) = c.day
WHERE o.id IS NULL AND c.day BETWEEN '2026-01-01' AND '2026-04-30';

Работает везде, не только Postgres.

Способ 3: LAG для gap detection

Найти пропуски между существующими датами:

WITH days AS (
    SELECT DISTINCT DATE(created_at) AS day FROM orders
),
gaps AS (
    SELECT day,
           LAG(day) OVER (ORDER BY day) AS prev_day,
           day - LAG(day) OVER (ORDER BY day) AS diff
    FROM days
)
SELECT * FROM gaps WHERE diff > 1;

Покажет даты, перед которыми был gap > 1 дня.

Найти непрерывные streaks

Обратная задача — найти последовательности:

WITH numbered AS (
    SELECT day, day - ROW_NUMBER() OVER (ORDER BY day) AS grp
    FROM days
)
SELECT MIN(day), MAX(day), COUNT(*) AS streak_len
FROM numbered
GROUP BY grp;

Классический pattern «gaps and islands».

С hourly данными

Для часовых пропусков:

SELECT generate_series(
    '2026-04-22 00:00'::TIMESTAMP,
    '2026-04-22 23:00'::TIMESTAMP,
    '1 hour'
) AS hour;

Потом LEFT JOIN.

На собесе

Вопрос: «Есть таблица orders с created_at. Найдите дни без заказов за последний месяц».

Идеальный ответ: generate_series (или calendar) → LEFT JOIN → WHERE NULL.

Если скажут «нет generate_series» — предложите CTE с рекурсией или calendar table.

Рекурсивный CTE (если нет generate_series)

WITH RECURSIVE dates AS (
    SELECT '2026-01-01'::DATE AS d
    UNION ALL
    SELECT d + 1 FROM dates WHERE d < '2026-04-30'
)
SELECT d.d FROM dates d
LEFT JOIN orders o ON DATE(o.created_at) = d.d
WHERE o.id IS NULL;

Работает в Postgres, MSSQL, SQLite.

Связанные темы

FAQ

Какой способ быстрее?

Calendar table — быстрее, если уже есть. Generate_series — удобнее на лету.

Gaps and islands — что это?

Pattern: найти «острова» непрерывных значений, разделённые «пропусками».

Работает в MySQL?

Generate_series нет, используйте calendar table или numbers table.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.