В предыдущем модуле мы говорили про шаблоны ETL и ELT — куда уезжают данные и где трансформируются. Теперь следующий уровень абстракции: как часто это происходит. Тут два больших мира: batch (пакетная обработка) и streaming (потоковая). Этот урок про batch — старший шаблон, самый распространённый и самый предсказуемый.
Что такое batch
Batch-обработка — это шаблон, в котором ты собираешь данные за определённый период времени (час, день, месяц) и обрабатываешь их одним пакетом на расписании. Не реагируешь на каждое событие в момент, когда оно произошло. Накопил, прогнал, сохранил результат.
Каждый job обрабатывает данные за фиксированное окно: вчерашние заказы, события за час, события за месяц.
Классический пример: ночной job в банке. В полночь закрывается операционный день, в 03:00 запускается процедура, которая считает агрегаты, обновляет балансы, готовит данные для отчёта CFO. К 06:00 все витрины обновлены. Аналитик утром открывает дашборд и видит вчерашние цифры.
Ключевое понятие: окно
Окно (window) — это период времени, который обрабатывает один запуск job-а. Окно определяется:
- Длиной: 1 час, 1 день, 1 неделя, 1 месяц.
- Границами: фиксированные (
[00:00, 24:00)) или скользящие. - Атрибутом времени: по
created_atсобытия, по дате загрузки, по дате партиции и т.д.
Самая частая модель — tumbling windows (непересекающиеся фиксированные окна). Например, окно «сутки» с 00:00 до 24:00. Каждые сутки обрабатываются ровно один раз, без пересечений.
# псевдокод
for date in calendar:
job(window_start=date.midnight, window_end=date.midnight + timedelta(days=1))
Реже встречаются sliding windows в batch — когда окна перекрываются, например «последние 30 дней» каждый день. Это нужно, например, для rolling-метрик типа «средняя выручка за 30 дней».
Расписание
Расписание (schedule) определяет, когда запускается job. Самый распространённый формат — cron-выражение:
0 4 * * * -- каждый день в 04:00
0 */6 * * * -- каждые 6 часов
0 0 1 * * -- первого числа каждого месяца в полночь
Расписание выбирается из бизнес-требований и операционных ограничений:
- Ежедневный отчёт CFO нужен к 09:00. Job запускается ночью, к утру всё готово.
- Маркетинг хочет видеть данные за последний час. Job каждый час.
- Биллинговая сводка раз в месяц. Job первого числа в полночь.
Расписание не должно совпадать с моментом окончания окна. Если окно «сутки 00:00-24:00», запускать job ровно в 24:00 опасно: данные могут ещё не прийти из источника. Обычно ставят буфер 1-4 часа: окно закончилось в 24:00, job запускается в 04:00, к этому времени все события дошли до источника.
Идемпотентность
Идемпотентность — критически важное свойство batch-job. Это значит: запуск одного и того же job на одних и тех же данных должен давать один и тот же результат, сколько бы раз его ни запустили.
Зачем это нужно. Job упал на середине из-за сбоя сети — нужно перезапустить. Кто-то обнаружил баг в логике месяц назад — нужно пересчитать данные за последний месяц. Поменялся источник — нужно перезаписать витрину. Без идемпотентности повторный запуск приведёт к дубликатам, неверным агрегатам или непредсказуемому состоянию.
Реализуется идемпотентность через несколько техник:
Перезапись партиции вместо вставки. Не INSERT INTO fact_orders ..., а INSERT OVERWRITE PARTITION (dt='2026-05-17') .... Если job упал и перезапустился, партиция полностью переписывается заново — никаких дубликатов.
-- идемпотентный INSERT
INSERT OVERWRITE TABLE fact_daily_orders
PARTITION (dt = '2026-05-17')
SELECT
customer_id,
COUNT(*) AS orders_count,
SUM(amount) AS revenue
FROM stg_orders
WHERE DATE(ordered_at) = '2026-05-17'
GROUP BY customer_id;
MERGE / UPSERT. Когда не используется партиционирование, можно делать MERGE по уникальному ключу — обновлять существующие строки, вставлять новые.
MERGE INTO dim_customers AS t
USING (SELECT * FROM stg_customers WHERE updated_dt = '2026-05-17') AS s
ON t.customer_id = s.customer_id
WHEN MATCHED THEN UPDATE SET
t.name = s.name,
t.segment = s.segment,
t.updated_at = s.updated_at
WHEN NOT MATCHED THEN INSERT (customer_id, name, segment, updated_at)
VALUES (s.customer_id, s.name, s.segment, s.updated_at);
Детерминированная логика. Если в job есть current_timestamp() или random() — результат разный при каждом запуске. Это не идемпотентно. Нужно передавать timestamp в параметрах job, а случайные значения избегать или фиксировать seed.
Late-arriving data
Late-arriving data (запоздалые данные) — это события, которые приходят в источник позже своего фактического времени. Классические причины:
- Мобильное приложение отправляет события, но пользователь был в самолёте — синхронизация произошла через сутки.
- Партнёрская система отдаёт данные с задержкой 3 дня по контракту.
- Сбой в очереди задержал доставку на несколько часов.
Это создаёт проблему: ты обработал окно 17 мая в 04:00 18 мая, посчитал агрегаты. А 20 мая в источнике появилось событие, фактически произошедшее 17 мая. Как с ним быть?
Событие фактически произошло 17 мая, но пришло в источник 20 мая. Агрегат за 17 мая уже посчитан и опубликован.
Стандартные подходы:
Skew window. Запускать job не сразу после окончания окна, а с задержкой — например, считать «вчерашний день» через двое суток после его окончания. Это даёт буфер для late-arriving событий, но задерживает свежесть данных.
Backfill. Когда приходит запоздалое событие, перезапускаешь job для той партиции, в которую оно попадает. Идемпотентность тут критична — повторный запуск перезаписывает партицию с новыми данными.
Append-only с created_at и event_at. В таблице хранятся обе даты: когда событие реально произошло (event_at) и когда оно загружено (loaded_at). Аналитика по event_at — для исторических метрик, по loaded_at — для операционных.
Повторяемость и backfill
Backfill — это процесс перепроцессинга исторических данных. Сценарии:
- Изменилась бизнес-логика, нужно пересчитать все витрины за последний год.
- Обнаружен баг в коде job-а, нужно перепроцессить последние 3 месяца.
- Добавлена новая колонка в фактовой таблице, нужно заполнить её исторически.
Backfill — это серия запусков job-а на исторических окнах:
for date in 2026-01-01..2026-05-17:
run_job(window_date=date)
Backfill работает только если job идемпотентный — иначе ты получишь дубликаты или мешанину. Поэтому идемпотентность — не приятный бонус, а обязательное условие production-ready batch-job-а.
Конкретный пример: ночной агрегатор заказов
Собираем всё в один сценарий. Компания работает с заказами, операционная база — Postgres, аналитическое DWH — Snowflake. Бизнес хочет видеть в Looker дневную выручку по сегментам клиентов.
Окно: сутки ([00:00, 24:00) UTC).
Расписание: каждый день в 04:00 UTC (буфер 4 часа на late-arriving).
Job-логика (псевдокод dbt-модели):
{{
config(
materialized='incremental',
unique_key='order_date_segment_pk',
incremental_strategy='delete+insert',
partition_by={'field': 'order_date', 'data_type': 'date'}
)
}}
SELECT
DATE(ordered_at) AS order_date,
customer_segment,
COUNT(*) AS orders_count,
SUM(amount) AS revenue,
CURRENT_TIMESTAMP AS loaded_at
FROM {{ ref('stg_orders') }}
WHERE
ordered_at >= DATEADD(day, -2, CURRENT_DATE)
{% if is_incremental() %}
AND DATE(ordered_at) NOT IN (SELECT DISTINCT order_date FROM {{ this }} WHERE order_date >= DATEADD(day, -2, CURRENT_DATE))
{% endif %}
GROUP BY 1, 2
Свойства:
- Окно фиксированное (сутки).
- Расписание (04:00) даёт буфер на late-arriving.
- Идемпотентный (delete+insert по партиции).
- Поддерживает backfill (можно прогнать на любую дату в прошлом).
Это типичный batch-pipeline, на котором стоит большая часть аналитической инфраструктуры в мире.
Airflow DAG: расписание и зависимости batch задачВ реальной работе DE 80% времени тратит на batch-pipelines. Streaming — модный и важный, но он редко полностью заменяет batch. Чаще они сосуществуют: streaming для операционных сценариев (фрод, алерты), batch для аналитики, отчётов, ML-фич, обучения моделей.
Попробуй сам
Подумай о компании, в которой ты работал, и попробуй угадать: какие там есть batch-пайплайны? Дневной агрегатор заказов? Биллинговая сводка раз в месяц? Расчёт KPI к утренней встрече? Затем мысленно представь, что один из них упал ночью — что нужно, чтобы безопасно перезапустить? Если ответ — «зависит от того, идемпотентен ли он» — значит ты понял главное.