Learning Platform
Глоссарий Troubleshooting
Урок 11.01 · 22 мин
Начальный
batchschedulingidempotencylate-arrivingwindowing

В предыдущем модуле мы говорили про шаблоны ETL и ELT — куда уезжают данные и где трансформируются. Теперь следующий уровень абстракции: как часто это происходит. Тут два больших мира: batch (пакетная обработка) и streaming (потоковая). Этот урок про batch — старший шаблон, самый распространённый и самый предсказуемый.

Что такое batch

Batch-обработка — это шаблон, в котором ты собираешь данные за определённый период времени (час, день, месяц) и обрабатываешь их одним пакетом на расписании. Не реагируешь на каждое событие в момент, когда оно произошло. Накопил, прогнал, сохранил результат.

Batch на временной шкале

Каждый job обрабатывает данные за фиксированное окно: вчерашние заказы, события за час, события за месяц.

00:00
окно 104:00 - 04:30 jobВ 04:00 запускается ночной job, который обрабатывает все события дня (с 00:00 до 23:59 предыдущей даты). Окно фиксировано — это сутки.
04:30
окно 2следующий деньЧерез сутки запускается следующий job. И так далее — каждые 24 часа.
событиезаказ в 14:23Конкретный заказ произошёл в 14:23. В batch-pipeline он попадёт в ночной job текущей даты.
ожиданиедо 04:00С 14:23 до 04:00 следующего дня заказ просто лежит в источнике. Batch не реагирует мгновенно.
попал в DWH04:30 утраПосле обработки заказ оказывается в агрегатной таблице вместе с тысячами других заказов того же дня.

Классический пример: ночной 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 первого числа в полночь.
NOTE

Расписание не должно совпадать с моментом окончания окна. Если окно «сутки 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 мая уже посчитан и опубликован.

17 маясобытие произошлоРеальное время события: 17 мая 14:00. Пользователь сделал покупку в офлайн-кассе.
18 мая 04:00job посчиталНочной job обработал данные за 17 мая, агрегаты опубликованы. Запоздалого события в них нет — оно ещё не дошло.
20 маясобытие пришлоКасса синхронизировалась только 20 мая. Событие появилось в источнике с created_at=17 мая.
?пересчёт?Что делать? Пересчитать партицию 17 мая? Игнорировать? Залить в новую партицию? Это и есть проблема late-arriving data.

Стандартные подходы:

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 задач
TIP

В реальной работе DE 80% времени тратит на batch-pipelines. Streaming — модный и важный, но он редко полностью заменяет batch. Чаще они сосуществуют: streaming для операционных сценариев (фрод, алерты), batch для аналитики, отчётов, ML-фич, обучения моделей.

Попробуй сам

Подумай о компании, в которой ты работал, и попробуй угадать: какие там есть batch-пайплайны? Дневной агрегатор заказов? Биллинговая сводка раз в месяц? Расчёт KPI к утренней встрече? Затем мысленно представь, что один из них упал ночью — что нужно, чтобы безопасно перезапустить? Если ответ — «зависит от того, идемпотентен ли он» — значит ты понял главное.

Проверка знанийKnowledge check
Почему идемпотентность критически важна для production-ready batch-job-а?
ОтветAnswer
Batch-job-ы регулярно перезапускают: после сбоев инфраструктуры, при обнаружении багов в логике, при backfill за исторический период, при изменениях схемы или бизнес-правил. Если job не идемпотентный — повторный запуск приводит к дубликатам, нарушенным агрегатам, непредсказуемому состоянию. Идемпотентность означает, что один и тот же job на одних и тех же данных даёт один и тот же результат — поэтому перезапуск безопасен. Реализуется через INSERT OVERWRITE по партициям, MERGE по уникальному ключу и детерминированную логику без current_timestamp() и random() внутри.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что такое идемпотентность batch-job-а?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4