Раздел 42 · Аналитика данных с нуля

generate_series: дыры в календаре

junior~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Отто · операционный директор доставки «Гонец»

Академический мы открыли 15 апреля, а в отчёте по дням он начинается 22-го. А у Химмаша в мае просто пропали три дня. Это дыры в учёте или в вашем отчёте?

В прошлом уроке мы считали заказы по дням, часам и неделям через GROUP BY. У этого способа есть слепое пятно, и Отто на него наткнулся.

Журнал, в котором нет прогулов

Представь школьный журнал, где учитель отмечает только тех, кто пришёл. Хочешь узнать, когда Петя прогуливал, а в журнале про прогулы ни строчки. Чтобы увидеть прогулы, нужен список всех учебных дней, а уже рядом с ним отметки.

GROUP BY и есть такой журнал. Он группирует строки, которые есть. Дня без заказов в таблице orders нет, значит, нет и группы:

SELECT created_at::date AS day, count(*) AS orders
FROM orders
WHERE zone_id = 7  -- Академический
GROUP BY 1
ORDER BY 1;
 day        | orders
------------+--------
 2026-04-22 |      1
 2026-04-23 |      1
 2026-04-24 |      1
 2026-04-25 |      3
 ...

15 апреля зона открылась, но в отчёте неделя тишины исчезла. Нужен список всех дней, «учебный календарь».

generate_series: календарь из воздуха

generate_series возвращает не одно значение, а набор строк. Поэтому её ставят во FROM, как таблицу:

SELECT generate_series(1, 3) AS n;
 n
---
 1
 2
 3

С датами то же самое, только шаг это интервал:

SELECT d::date AS day
FROM generate_series(date '2026-04-15', date '2026-04-17', interval '1 day') AS d;
 day
------------
 2026-04-15
 2026-04-16
 2026-04-17

Без ::date строки придут как timestamptz с полуночью: функция считает дату плюс интервал моментом времени. Для дневного календаря приводим к date.

Дни без заказов

Теперь у нас есть журнал всех дней. Осталось найти те, для которых нет ни одного заказа. Это вопрос «существует ли», и мы уже умеем задавать его через NOT EXISTS:

SELECT d::date AS day
FROM generate_series(date '2026-04-15', date '2026-06-30', interval '1 day') AS d
WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.zone_id = 7 AND o.created_at::date = d::date
)
ORDER BY 1;
 day
------------
 2026-04-15
 2026-04-16
 2026-04-17
 2026-04-18
 2026-04-19
 2026-04-20
 2026-04-21
 2026-04-28

Восемь пустых дней: первая неделя после открытия целиком и 28 апреля. created_at записано по местным часам, поэтому created_at::date даёт местную дату, ровно то, что нужно Отто.

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.