Learning Platform
Глоссарий Troubleshooting
Урок 03.05 · 25 мин
Средний
lookbacklate-arriving datawatermarkout-of-order events

Lookback и late-arriving data

В уроке про append мы упомянули late-arriving events — события, которые приходят задним числом. На production-проекте это не edge case, а ежедневная реальность: Fivetran иногда задерживает sync на 24 часа, Kafka consumer retry’ит batch’и через час после originals, мобильное приложение копит события офлайн и присылает через сутки.

Если ваша incremental-модель не учитывает late-arriving data, она тихо теряет данные. Этот урок — про паттерн lookback, который решает проблему, и про trade-offs его использования.

Что такое late-arriving data

Late-arriving event — это запись с event_timestamp в прошлом, но loaded_at (timestamp поступления в warehouse) в настоящем.

Жизненный цикл late-arriving event

Событие случилось 5 дней назад, но в warehouse прилетело только сегодня. Incremental без lookback его пропустит.

2026-05-14 10:00Пользователь нажал кнопку. Мобильное приложение записало event локально, потому что был оффлайн
оффлайн
2026-05-19 14:00Приложение online, отправило накопленные events в Kafka. EL-инструмент через час забрал их в warehouse
dbt incrementalLast run был сегодня в 13:00. max(event_timestamp) в target = 2026-05-19 13:00. Фильтр event_timestamp > max отсечёт строку с timestamp 2026-05-14
отсечёт
События нет в targetMart counts на 1 меньше реальности. Через месяц дашборд показывает заниженный engagement, никто не понимает причину

Это silent data loss. Никакая dbt-ошибка не выскочит, downstream-метрики просто будут заниженными на 1-5%. На больших масштабах это превращается в потерянный миллион долларов revenue, которую вы не учли.

Lookback pattern — recompute window

Lookback — это расширение окна incremental-фильтра, чтобы охватить возможные late-arrivals:

{{ config(
    materialized='incremental',
    incremental_strategy='merge',
    unique_key='event_id'
) }}

select
    event_id,
    user_id,
    event_type,
    event_timestamp,
    loaded_at
from {{ source('events', 'raw_events') }}

{% if is_incremental() %}
  where event_timestamp > (
      (select max(event_timestamp) from {{ this }}) - interval '3 days'
  )
{% endif %}

Ключевая строка — - interval '3 days'. Это lookback window. Вместо event_timestamp > max(event_timestamp), мы говорим event_timestamp > max - 3 дня.

Что это даёт:

  • На каждом run перечитываются последние 3 дня source-данных (а не только новые с last run).
  • Если в эти 3 дня прилетел late-event с timestamp 2 дня назад — он будет в дельте.
  • MERGE по unique_key либо INSERT (если event_id новый), либо UPDATE (если уже есть).

Late-events ловятся, дубли не создаются, спасибо MERGE.

Lookback с append — особый случай

Если стратегия append, MERGE/dedup нет. Lookback с append даёт дубли: ивенты за последние 3 дня перечитываются и инсертятся повторно.

Решение — append + dedup на следующем слое:

-- staging: append с lookback (приймет дубли)
{% if is_incremental() %}
  where event_timestamp > (
      (select max(event_timestamp) from {{ this }}) - interval '3 days'
  )
{% endif %}

-- intermediate: dedup через qualify
select *
from {{ ref('stg_events') }}
qualify row_number() over (
    partition by event_id 
    order by loaded_at desc
) = 1

Staging имеет дубли, intermediate их убирает. Это eventually consistent: после каждого run intermediate показывает правильные данные. Производительность хуже append без lookback, но всё ещё лучше merge.

Какое окно lookback

Окно — это компромисс между полнотой и компьютом:

Окна lookback по сценариям

Выбор окна зависит от источника и приемлемого latency. Меньше — быстрее, но риск пропусков. Больше — медленнее, но безопаснее.

1 деньМинимум lookback. Покрывает только intraday delays. Для Kafka с low-latency consumer ок, для Fivetran недостаточно
3-7 днейProduction default. Покрывает Fivetran-задержки, mobile offline-events, retry circles. Compute умеренный
30 днейДля медленных систем: legacy database replication, monthly batch jobs. Run медленнее, но safety net большой
Не использоватьОкно 90+ дней практически равно full-refresh. Подумайте, не лучше ли периодический full-refresh раз в месяц

Эмпирические rules of thumb:

  • Real-time event streams (Kafka, Kinesis): 1-2 дня.
  • Fivetran/Airbyte EL-инструменты: 3-7 дней.
  • Mobile/IoT events с offline queueing: 7-14 дней.
  • Legacy ETL pipelines (раз в день, иногда падают): 14-30 дней.

Late-arriving facts vs late-arriving dimensions

Data Modeling: late-arriving facts — теоретическая база

В Kimball-терминологии есть два типа late-arrivals:

  1. Late-arriving facts — событие пришло задним числом. Решается lookback в fact-таблице.
  2. Late-arriving dimensions — dimension-запись обновилась задним числом (например, customer изменил адрес ретроактивно). Решается через SCD2 snapshots (модуль 04) с правильной обработкой.

Этот урок про facts. Dimensions — в модуле snapshots.

Тестирование late-arriving handling

Как проверить, что lookback работает?

Тест 1: имитация late-event

  1. Запустите dbt run на свежей модели. Зафиксируйте row count в target (N).
  2. Вручную вставьте в source раздачу с event_timestamp = 2 дня назад (внутри lookback window):
    INSERT INTO source.raw_events VALUES 
        ('test-event-1', 'user-42', 'click', now() - interval '2 days', now());
  3. Запустите dbt run снова.
  4. Проверьте row count в target — должен быть N+1.
  5. Проверьте, что эта event_id присутствует: select * from target where event_id = 'test-event-1'.

Тест 2: имитация out-of-window event

  1. Снова вставьте раздачу, но с event_timestamp = 30 дней назад (вне lookback 7 дней):
    INSERT INTO source.raw_events VALUES 
        ('test-event-2', 'user-42', 'click', now() - interval '30 days', now());
  2. Запустите dbt run.
  3. Эта event НЕ должна появиться в target — она вне lookback.
  4. Это expected behavior — lookback не gracefully handle очень старые late events, и это компромисс.

Тест 3: dbt unit test (модуль 08)

Лучший способ — dbt unit tests с fixtures, которые включают late-arriving rows. Разберём в модуле 08.

Backfill для events вне lookback

Если событие пришло с timestamp 30 дней назад, а lookback window 7 дней — оно потеряется. Что делать?

Решение 1: широкое lookback. Если ваши источники могут опаздывать на 30 дней, поставьте lookback 35-40 дней. Компьют больше, но safety.

Решение 2: periodic full-refresh. Раз в месяц запускайте dbt run --full-refresh для критичных моделей. Это пересчитает с нуля, поймает все late events за весь период. Минус — full-refresh дорогой и долгий.

Решение 3: hybrid — обычно lookback 7 дней, раз в неделю —full-refresh. Это даёт лучшее обоих миров: ежедневная latency хорошая, недельный full-refresh ловит outliers.

Решение 4: out-of-window alerts. Detect события с timestamp вне lookback при загрузке, отправлять в dead-letter table. Дальше — manual или scheduled backfill.

DuckDB-специфика

  • Lookback работает на DuckDB идентично Snowflake/BQ.
  • На DuckDB обычно нет partitioning, поэтому lookback с большим окном (30 дней) сканирует много source — может быть медленным.
  • На MotherDuck (DuckDB cloud) производительность сопоставима с Snowflake-small warehouse.

Production gotchas

1. Lookback без unique_key с merge

{% if is_incremental() %} с lookback читает overlapping window. Если используете append, получаете дубли (мы об этом говорили). Если merge без unique_key — config error. Lookback требует merge с правильным unique_key.

2. Lookback window меньше чем delay источника

EL-инструмент задерживает sync на 5 дней (плохая интеграция), а lookback 3 дня. Каждый день теряется по 1-2 дня данных. Diagnose: replicate-lag metric источника. Если > lookback — увеличить window.

3. Lookback на маленькой таблице — overhead

На source с 100K строк в день lookback 7 дней — это 700K чтений на каждый run. Если таблица растёт быстро (миллион строк/день), 7M чтений — это уже 5-10 секунд. На большом числе incremental-моделей в проекте — суммарная задержка.

4. Lookback несовместим с partition pruning

Если incremental_predicates узкое окно (1 день), а WHERE-фильтр в SELECT — широкое (7 дней), perf-выигрыша от predicates нет — warehouse всё равно сканирует 7 дней. Lookback и incremental_predicates должны быть согласованы по окну.

5. Lookback с changing schema

Если в source поменялась колонка (добавили referrer_url), lookback переиспользует старые строки за 7 дней. У старых строк referrer_url = NULL. В target — смесь старого и нового. Решение — on_schema_change='append_new_columns' + явная обработка NULL’ов в новой колонке.

Production-обычай: lookback в YAML, не в SQL

В крупных проектах хорошо иметь lookback как параметр модели в _models.yml:

- name: fct_events
  config:
    materialized: incremental
    incremental_strategy: merge
    unique_key: event_id
    pre_hook: "{% set lookback_days = var('events_lookback_days', 7) %}"

И в SQL:

{% if is_incremental() %}
  where event_timestamp > (
      (select max(event_timestamp) from {{ this }}) 
      - interval '{{ var("events_lookback_days", 7) }} days'
  )
{% endif %}

Это даёт возможность через --vars events_lookback_days: 30 запустить с расширенным lookback для backfill, не меняя код.

Попробуй сам

  1. На вашей самой критичной incremental-модели измерьте late-arriving rate: запустите backfill с 30-дневным lookback и сравните row counts. Разница — это events, которые регулярно теряются.
  2. Выберите правильное lookback окно на основе source-delay metrics (Fivetran sync history, Kafka consumer lag).
  3. Реализуйте dbt unit test, который проверяет late-arriving handling: fixture с event timestamp 2 дня назад, проверка что он в output.
  4. Добавьте var-based lookback в config — для гибкости при backfill.
Проверка знанийKnowledge check
Команда добавила lookback 7 дней на fct_events. Через неделю Senior жалуется: 'dbt run теперь занимает на 50% дольше, при этом мы видим тех же late events в выходные. Можем ли уменьшить lookback до 3 дней?'. Что ответить?
ОтветAnswer
Это правильный вопрос — lookback это компромисс, и узнать оптимальное окно можно только из данных. Что сделать перед тем, как менять с 7 на 3 дней. Первое — измерить distribution late-events: 'SELECT date_diff(now(), event_timestamp, day) AS lag_days, count(*) FROM fct_events WHERE loaded_at > now() - interval 30 days GROUP BY lag_days ORDER BY lag_days'. Это покажет, какой процент events имеет lag 1, 2, ... N дней. Если 99% events имеют lag <= 3 дня, можно безопасно снизить lookback. Если 5% events имеют lag 5-7 дней — снижение до 3 дней потеряет эти 5%. Второе — проверить, могут ли быть outliers с lag > 7 дней (например, monthly batch jobs с rare delays). Если да — лучше оставить 7 дней или добавить periodic full-refresh раз в месяц для catch-all. Третье — оценить cost-benefit. Если уменьшение с 7 до 3 дней даёт 50% speedup (с 30 минут до 15 минут) и теряем 0.5% late-events — приемлемо для большинства бизнес-метрик. Если теряем 5% — это материальный data loss, перевешивает speedup. Решение — data-driven: измерить distribution, оценить acceptable data loss, прийти к консенсусу с downstream-командой (BI/finance), и только потом менять параметр. Не делать changes 'на ощущении', что 'выглядит много'. На вопрос Senior'а — 'давайте сначала проверим distribution late-events, и если 99% укладываются в 3 дня, снижение даст 50% выигрыш без значимой потери'.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что такое late-arriving event?

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

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

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

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