Условие
Объясните разницу между ETL (Extract → Transform → Load) и ELT (Extract → Load → Transform). Почему в эпоху облачных DWH чаще выбирают ELT? Приведите два use case, где ETL остаётся правильным выбором.
Решение
Подход
ETL (классика 2000-х): данные забирают из источника, трансформируют на промежуточном сервере (Informatica, SSIS, Talend, Python-скрипт) и загружают уже очищенные витрины в DWH. Сырьё в DWH не попадает.
ELT (современность с 2015+): данные забирают и сырьём загружают в DWH (Snowflake, BigQuery, Redshift). Все трансформации делают SQL'ом внутри DWH (часто через dbt). Сырьё остаётся доступным.
Почему ELT победил
- Дешёвое хранение в облаке (S3, GCS) — не страшно хранить сырьё.
- Эластичный compute у MPP-DWH — трансформации на 10 ТБ выполняются за минуты.
- Аналитики знают SQL — не нужен отдельный python/scala-инженер на каждый pipeline.
- dbt + git — code review, тесты, документация трансформаций «из коробки».
- Время backfill: можно перезапустить только модель, а не весь пайплайн.
Когда ETL всё ещё уместен
- Compliance / PII: нельзя загружать персональные данные в DWH в сыром виде (GDPR, 152-ФЗ). Маскируем/хэшируем до загрузки.
- Очень большие объёмы при ограниченном бюджете DWH: миллионы JSON в час — дешевле сжать/агрегировать в Spark и положить в DWH уже свёрнутыми.
- Real-time pipeline: Kafka → Flink → ClickHouse (трансформации в потоке, в DWH летят готовые события).
- Источник с rate-limit API: имеет смысл сразу нормализовать и не хранить «сырьё» (которое всё равно недоступно для перезапроса).
Пример ELT с dbt
-- models/staging/stg_orders.sql
{{ config(materialized='view') }}
SELECT
id AS order_id,
user_id,
created_at::timestamp AS created_at,
amount::numeric(18, 2) AS amount,
status
FROM {{ source('raw', 'orders') }}
WHERE created_at >= '2024-01-01';-- models/marts/fct_orders_daily.sql
{{ config(materialized='incremental', unique_key='dt') }}
SELECT
created_at::date AS dt,
COUNT(*) AS n_orders,
SUM(amount) AS gmv,
COUNT(DISTINCT user_id) AS dau
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
WHERE created_at::date >= (SELECT MAX(dt) FROM {{ this }}) - INTERVAL '3 day'
{% endif %}
GROUP BY 1;Подводные камни
- ELT без quality-тестов = «мусор на входе, мусор на выходе». В dbt обязательны
unique,not_null,accepted_values,relationships. - Cost runaway: некомпетентный аналитик может вкинуть
SELECT *по 10 ТБ — счёт за день вырастет в 10×. - PII в Bronze: если в сырьё попали паспорта/телефоны без маскирования — это нарушение. Делать masking views поверх.
- dbt incremental + late-arriving data: при
unique_keyбезmergeстроки задним числом не обновятся. - «ELT = всегда лучше» — нет. Для очень больших batch-нагрузок Spark по-прежнему дешевле.
- Backfill в incremental-модели dbt: требует
--full-refreshили ручногоDELETE+ ре-run.
Эталонный ответ
ELT — сначала загрузка сырья в DWH, потом трансформации SQL'ом (dbt). Выигрывает за счёт дешёвого хранения, эластичного compute и SQL-навыков аналитиков.
ETL остаётся уместным при: (1) compliance с PII — маскирование до загрузки; (2) очень больших объёмах с дорогим DWH — агрегируем в Spark; (3) real-time потоках; (4) скудных источниках без re-fetch.