Сначала зафиксируйте grain данных, допущения, метрику и риск leakage, затем обсуждайте модель, инструмент или инфраструктуру.
Вопросы и ответы
11 подробных ответов
01В чём разница между ETL и ELT, почему ELT победил с приходом облачных хранилищ и когда ETL всё ещё оправдан?
middle
Короткий ответ: ETL трансформирует данные до загрузки (на отдельном движке), ELT сначала грузит сырые данные в хранилище, а трансформации выполняет уже внутри него на SQL. ELT стал стандартом, потому что облачные MPP-хранилища (Snowflake, BigQuery, Redshift) дёшево масштабируют вычисления и разделяют storage/compute.
Подробно:
- ETL — трансформация на промежуточном движке (раньше — дорогие ETL-серверы). В хранилище попадают только готовые, «чистые» данные. Минус: логика заперта в инструменте, а сырьё теряется.
- ELT — грузим сырой слой как есть, трансформируем через dbt/SQL. Плюс: сырьё сохраняется, трансформации версионируются в Git, масштабирование берёт на себя хранилище.
- Почему ELT победил — разделение storage/compute сделало вычисления в хранилище дешёвыми и эластичными; аналитику стало удобнее держать в SQL.
| Критерий | ETL | ELT |
|---|---|---|
| Где трансформация | На отдельном движке | Внутри хранилища |
| Сырой слой | Обычно теряется | Сохраняется |
| Масштабирование | Ограничено движком | Эластичное (MPP) |
| Типичный стек | Informatica, SSIS | Fivetran + dbt + Snowflake |
Когда ETL всё ещё нужен: тяжёлые преобразования до попадания в хранилище, маскирование PII/комплаенс (нельзя грузить сырьё), или источник, который проще привести к схеме на лету.
⚠️ Частая ошибка: думать, что ELT — это «без трансформаций». Трансформации никуда не делись, они просто переехали в хранилище и выполняются после загрузки.
02Пакетная (batch) и потоковая (streaming) обработка: какие компромиссы по задержке, стоимости и сложности и как выбрать?
middle
Короткий ответ: Batch обрабатывает данные порциями по расписанию (задержка минуты–часы, просто и дёшево), streaming обрабатывает события по мере поступления (задержка секунды, но дороже и сложнее в эксплуатации). Выбор диктует требование к свежести данных, а не мода.
Подробно:
- Batch — читаем накопленный объём за интервал, гоняем через Airflow/dbt/Spark. Проще отлаживать, легко делать бэкфилл, дешевле. Задержка = размер окна.
- Streaming — Kafka + Flink/Spark Structured Streaming обрабатывают события непрерывно. Нужны для фрода, алертов, real-time дашбордов. Требуют работы с состоянием, водяными знаками для опоздавших событий, exactly-once.
- Микро-батч — компромисс: обработка маленькими окнами (секунды), проще стриминга при почти той же свежести.
| Критерий | Batch | Streaming |
|---|---|---|
| Задержка | Минуты–часы | Секунды |
| Стоимость | Ниже | Выше (24/7) |
| Сложность | Низкая | Высокая (state, опоздавшие) |
| Бэкфилл | Тривиальный | Сложный |
| Когда | Отчётность, ML-фичи | Фрод, алерты, real-time |
Как выбрать: начни с вопроса «насколько свежими должны быть данные для решения?». Если ответ «часа достаточно» — batch. Секунды и деньги на кону — streaming.
⚠️ Частая ошибка: строить стриминг, потому что «это современно», хотя бизнесу хватает часовой свежести — вы платите за сложность и деньги без ценности.
03Что такое идемпотентность в пайплайнах, почему она критична и как сделать загрузку идемпотентной?
senior
Короткий ответ: Идемпотентная загрузка даёт один и тот же результат при повторном запуске на тех же данных — второй прогон не создаёт дублей. Это критично, потому что таски падают и ретраятся, а без идемпотентности ретрай удваивает данные.
Подробно:
- Overwrite партиции — вместо
INSERTцеликом перезаписывай партицию за день:INSERT OVERWRITE PARTITION. Повтор просто затирает старое той же порцией. - MERGE / upsert по ключу — сопоставляй по бизнес-ключу и обновляй, а не вставляй вслепую. Повтор обновит те же строки.
- Дедупликация по ключу — если источник даёт дубли, оставляй последнюю версию через
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC).
-- Идемпотентно: перезапись партиции за конкретную дату
INSERT OVERWRITE TABLE sales PARTITION (dt = '2026-06-30')
SELECT * FROM staging_sales WHERE dt = '2026-06-30';
-- Идемпотентно: upsert по бизнес-ключу
MERGE INTO dim_customer t
USING staging_customer s
ON t.customer_id = s.customer_id
WHEN MATCHED THEN UPDATE SET t.name = s.name, t.updated_at = s.updated_at
WHEN NOT MATCHED THEN INSERT (customer_id, name, updated_at)
VALUES (s.customer_id, s.name, s.updated_at);
⚠️ Частая ошибка: слепой INSERT INTO ... SELECT без ключа или перезаписи партиции. При ретрае упавшего таска строки вставляются повторно — получаете тихие дубли, которые всплывают в отчётах как задвоенная выручка.
04Какие есть стратегии инкрементальной загрузки: полная против инкрементальной, водяной знак и CDC?
senior
Короткий ответ: Полная загрузка каждый раз перечитывает весь источник — просто, но не масштабируется. Инкрементальная забирает только новое/изменённое с прошлого прогона по водяному знаку (high-water mark), а CDC ловит сами изменения (insert/update/delete) из лога БД.
Подробно:
- Full load —
TRUNCATE+ перезалив. Работает на маленьких таблицах, честен по удалениям, но дорог и медленен на больших. - Инкрементальная по водяному знаку — храним максимум
updated_at/idпрошлого прогона и берём строки строго больше него. Дёшево, но не видит физическихDELETE. - CDC — читаем redo/WAL-лог (Debezium, Fivetran) и применяем поток изменений, включая удаления. Ближе к real-time, но сложнее в эксплуатации.
Водяной знак (high-water mark):
прошлый прогон: max(updated_at) = 2026-06-29 23:00
│
▼
SELECT * FROM src
WHERE updated_at > '2026-06-29 23:00' ← только дельта
│
▼
MERGE в целевую таблицу → новый watermark = 2026-06-30 23:00
⚠️ Частая ошибка: инкремент по updated_at, когда источник не проставляет его при всех изменениях (или использует нестрогое >=, ловя дубли на границе). Физические удаления так тоже теряются — для них нужен CDC или периодический full-reconcile.
05Как безопасно сделать бэкфилл пайплайна: переобработка истории, партиционные бэкфиллы и как не задвоить данные?
senior
Короткий ответ: Бэкфилл — это переобработка исторических периодов при изменении логики или заполнении пропусков. Делай его безопасно только через идемпотентную перезапись по партициям: каждый день пересчитывается независимо и затирает свою партицию, а не добавляет строки.
Подробно:
- Партиционируй по дате — таск должен принимать дату как параметр и трогать только партицию этого дня (
INSERT OVERWRITE PARTITION (dt=...)). Тогда бэкфилл = прогон того же таска за прошлые даты. - Идемпотентность обязательна — перезапись, а не
INSERT. Иначе повторный прогон за уже посчитанный день удвоит данные. - Контролируй нагрузку — ограничивай параллелизм (в Airflow
max_active_runs), чтобы бэкфилл 2 лет не положил кластер и источник.
Партиционный бэкфилл (переобработка за диапазон дат):
for dt in 2026-01-01 .. 2026-06-30:
OVERWRITE partition(dt) ← каждый день независим и идемпотентен
┌────────┬────────┬────────┬────────┐
│ dt=1 │ dt=2 │ dt=3 │ ... │ перезапись, не добавление
└────────┴────────┴────────┴────────┘
⚠️ Частая ошибка: двойной счёт при бэкфилле — запустить пересчёт поверх уже загруженных данных через INSERT вместо OVERWRITE, либо агрегаты, которые суммируют старые и новые строки. Всегда переписывай партицию целиком.
06Как устроена оркестрация в Airflow (DAG, таски, операторы, расписание) и чем он отличается от Dagster/Prefect?
middle
Короткий ответ: Airflow описывает пайплайн как DAG — граф тасков с зависимостями; каждый таск создаётся оператором и выполняется по расписанию. Ключевой принцип — таски должны быть идемпотентными и ретраебельными, потому что Airflow их перезапускает при сбоях.
Подробно:
- DAG — направленный ацикличный граф: узлы это таски, рёбра — зависимости (
a >> b). Планировщик запускает таск, когда выполнены его upstream-зависимости. - Операторы — шаблоны тасков:
PythonOperator,BashOperator, сенсоры, специфичные под сервисы. Каждый run привязан кlogical_date(интервалу), что и делает бэкфилл возможным. - Идемпотентность и ретраи — задаёшь
retries; таск обязан безопасно перезапускаться, поэтому пишем через overwrite партиции.
from airflow import DAG
from airflow.operators.python import PythonOperator
import pendulum
with DAG(
dag_id="daily_sales",
schedule="@daily",
start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
catchup=True, # позволяет бэкфилл прошлых интервалов
default_args={"retries": 2},
) as dag:
extract = PythonOperator(task_id="extract", python_callable=do_extract)
load = PythonOperator(task_id="load", python_callable=do_load)
extract >> load # зависимость: load после extract
Airflow vs Dagster/Prefect: Airflow — зрелый стандарт, ориентирован на таски и расписание. Dagster мыслит ассетами данных и типизацией/тестами; Prefect делает упор на динамические потоки и удобный Python-API. Для новых проектов часто берут Dagster ради data-assets, но Airflow остаётся дефолтом по экосистеме.
⚠️ Частая ошибка: таск, который нельзя перезапустить (например, дописывает в файл или шлёт письмо на каждый run). При первом же ретрае получаете дубли или спам.
07Что даёт dbt как слой трансформаций в ELT: модели, ref, тесты, материализации — и почему его любят аналитики и инженеры?
middle
Короткий ответ: dbt — это слой T в ELT: ты пишешь трансформации как SQL-SELECT-модели, а dbt строит из них граф зависимостей через ref(), материализует результат в хранилище и прогоняет тесты. Любят за то, что аналитика превращается в версионируемый, тестируемый, документированный код.
Подробно:
- Модели и ref — каждая модель это
SELECT; ссылки через{{ ref('stg_orders') }}дают dbt DAG и порядок запуска. Никаких ручныхCREATE TABLE. - Материализации —
view(лёгкая, всегда свежая),table(быстрое чтение),incremental(досчитывает только новое по watermark),ephemeral(CTE). Выбор — это баланс цены пересчёта и скорости чтения. - Тесты и документация —
not_null,unique,relationshipsпрямо в YAML; плюс автогенерация документации и lineage.
# models/marts/orders.yml
models:
- name: orders
config:
materialized: incremental
unique_key: order_id # апсерт по ключу при инкременте
columns:
- name: order_id
tests: [unique, not_null]
- name: customer_id
tests:
- relationships:
to: ref('customers')
field: customer_id
| Материализация | Когда |
|---|---|
| view | Лёгкие трансформации, нужна свежесть |
| table | Тяжёлое чтение, частые запросы |
| incremental | Большие таблицы фактов, только дельта |
⚠️ Частая ошибка: делать table там, где хватает view, и наоборот гонять incremental без корректного unique_key — тогда инкремент задваивает строки при повторной обработке.
08Как спроектировать надёжный DAG пайплайна: зависимости, ретраи, алертинг, атомарность и почему нельзя делать один гигантский таск?
senior
Короткий ответ: Хороший DAG разбит на маленькие атомарные идемпотентные таски с явными зависимостями, у каждого — ретраи и алерты. Монолитный таск — антипаттерн: при падении на 90% приходится перезапускать всё с нуля, а точки сбоя не видно.
Подробно:
- Дроби на атомарные таски — extract, load, transform, quality-check раздельно. Падение на transform не заставляет заново тянуть extract; ретраи прицельные.
- Явные зависимости — рёбра графа отражают реальный порядок данных; никаких скрытых связей через «сработает по времени».
- Атомарность записи — таск либо полностью применил результат (overwrite партиции), либо ничего. Никаких полузаписанных состояний.
- Ретраи + алертинг — экспоненциальный backoff на транзиентные сбои; алерт в Slack/PagerDuty на финальный fail и на SLA-мисс.
Плохо (монолит) Хорошо (атомарные таски)
┌──────────────┐ extract ─► load ─► transform ─► test ─► publish
│ do_everything│ │ │ │ │
│ (падает на │ retry retry retry alert on fail
│ 90% → всё │
│ заново) │ падение на transform ⇒ ретраится только transform
└──────────────┘
⚠️ Частая ошибка: один «сделай-всё» таск на 500 строк без промежуточных состояний. Любой сбой = полный перезапуск, невозможно понять, где именно сломалось, и нельзя переиспользовать шаги.
09Планирование и триггеры пайплайнов: cron/интервал против событий и сенсоров, и как обрабатывать опоздавшие данные?
middle
Короткий ответ: Пайплайн запускают либо по расписанию (cron/интервал — просто и предсказуемо), либо по событию (сенсор ждёт появления файла/сообщения/готовности upstream — точнее, но сложнее). Опоздавшие данные обрабатывают перезапуском партиции их периода или окнами с водяным знаком.
Подробно:
- Cron / интервал — «каждый день в 03:00». Плюс: предсказуемость и простота. Минус: может стартовать раньше, чем данные реально готовы.
- Событийные / сенсоры — таск ждёт триггер: прилетел файл в S3, сообщение в очереди, завершился upstream-DAG. Точнее по готовности, но требует управлять таймаутами и «завис навсегда».
- Опоздавшие данные (late-arriving) — данные за вчера пришли сегодня. Решения: идемпотентный перезапуск вчерашней партиции, скользящее окно пересчёта (последние N дней), или водяной знак в стриминге, допускающий opоздание.
| Тип запуска | Плюсы | Минусы |
|---|---|---|
| Cron/интервал | Просто, предсказуемо | Данные могут быть не готовы |
| Сенсор/событие | По факту готовности | Таймауты, сложнее отладка |
⚠️ Частая ошибка: жёсткий cron без проверки готовности upstream — таск стартует по времени, читает половину данных и тихо выдаёт неполный результат. Либо сенсор без таймаута, который висит и держит слот планировщика.
10Гарантии доставки в пайплайне: at-least-once против exactly-once и как достичь effectively-once через идемпотентные записи?
senior
Короткий ответ: At-least-once гарантирует, что событие не потеряется, но допускает дубли при ретраях; exactly-once — ровно одна обработка, но дорого и не всегда достижимо в распределёнке. На практике целятся в effectively-once: at-least-once доставка плюс идемпотентная запись, дающая эффект единственной обработки.
Подробно:
- At-most-once — максимум одна попытка, при сбое событие теряется. Годится, только если потери не критичны.
- At-least-once — ретраим до успеха, потерь нет, но возможны дубли. Дефолт большинства очередей (Kafka, SQS).
- Exactly-once — ровно один раз end-to-end. Дорого: нужны транзакции, идемпотентные продюсеры, координация. В чистом виде редко нужен.
- Effectively-once — принимаем дубли на доставке, но гасим их на записи: upsert/MERGE по ключу, дедуп по event_id, overwrite партиции. Результат неотличим от exactly-once.
| Гарантия | Потери | Дубли | Цена |
|---|---|---|---|
| At-most-once | Да | Нет | Низкая |
| At-least-once | Нет | Да | Средняя |
| Exactly-once | Нет | Нет | Высокая |
| Effectively-once | Нет | Гасятся на записи | Средняя |
⚠️ Частая ошибка: обещать «exactly-once», а на деле иметь at-least-once с неидемпотентной записью — дубли из очереди спокойно долетают до витрины и раздувают метрики.
11Как устроена слоистая архитектура пайплайна (raw/staging/curated, или bronze/silver/gold) и зачем хранить неизменяемый сырой слой?
middle
Короткий ответ: Данные проходят слои: raw/bronze — сырьё как из источника, staging/silver — очищенное и типизированное, curated/gold — бизнес-витрины для потребителей. Неизменяемый сырой слой держат, чтобы можно было переиграть любую трансформацию заново, не обращаясь повторно к источнику.
Подробно:
- Bronze (raw) — данные как есть, append-only, без изменений. Это ваша «точка правды» и страховка.
- Silver (staging) — очистка, приведение типов, дедупликация, стандартизация ключей. Здесь исправляют качество.
- Gold (curated) — агрегаты и витрины под конкретные метрики/дашборды. То, что видит бизнес.
Источник
│
▼
┌─────────┐ очистка, ┌─────────┐ агрегация, ┌────────┐
│ BRONZE │─ типизация ──►│ SILVER │─ бизнес- ───►│ GOLD │──► BI / ML
│ (raw, │ дедуп │(staging,│ логика │(витрины│
│ immutab)│ │ чистый) │ │ метрик)│
└─────────┘ └─────────┘ └────────┘
переиграть всё заново можно с bronze, не дёргая источник
Зачем immutable raw: источник может измениться или стать недоступен; при новой ошибке в логике вы пересчитываете silver/gold из bronze. Плюс аудит и воспроизводимость.
⚠️ Частая ошибка: грузить сразу в curated без сырого слоя. Нашли баг в трансформации — переиграть нечем, исторические данные из источника уже не достать, и приходится жить с испорченными витринами.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.