ETL: что это простыми словами, этапы и чем ETL отличается от ELT
ETL — процесс, который забирает данные из источников, приводит их в порядок и загружает в хранилище. Этапы extract, transform, load на примере продукта, разница ETL и ELT, инкрементальная загрузка, идемпотентность и SQL-проверки качества данных.
Содержание статьи
Понедельник, утро. Продакт присылает скриншот дашборда: в воскресенье 30 августа активных пользователей 223, а неделей раньше, тоже в воскресенье, было 495. Продукт упал? Нет. Выгрузку закрыли в 13:00, и на дашборд приехала только первая половина дня. Такие вопросы аналитик разбирает постоянно, и часто ответ лежит не в продукте, а в конвейере, который возит данные. ETL (extract, transform, load) — это процесс, который забирает данные из источников, приводит их к единому виду и загружает в хранилище, откуда их читают отчёты и дашборды.
Коротко
- ETL — три шага: извлечь данные из источников, преобразовать их и загрузить в хранилище.
- В ELT порядок другой: сырые данные сначала загружают в хранилище, а преобразуют уже там, обычно SQL-запросами.
- Большинство конвейеров грузит данные не целиком, а приращениями: только то, что появилось после прошлого запуска.
- Хороший конвейер идемпотентен: повторный запуск за тот же период не создаёт дублей.
- После каждой загрузки нужны проверки: свежесть, объём, пустые поля, дубли, «осиротевшие» строки.
- Если метрика резко изменилась в последний день, сначала проверьте, закончился ли этот день в данных.
Что такое ETL простыми словами
Данные о продукте рождаются в разных местах. Приложение пишет события: пользователь открыл экран, создал отчёт, отправил приглашение. Биллинг хранит оплаты. CRM знает, из какой компании клиент. Каждая система хранит данные так, как удобно ей, и ни одна не собиралась отвечать на вопрос «сколько выручки принёс канал за август».
ETL-процесс собирает эти данные в одно место и делает их пригодными для анализа. Extract — извлечь: забрать новые записи из базы приложения, API биллинга, файлов. Transform — преобразовать: привести типы и часовые пояса, убрать дубли, склеить оплату с пользователем, посчитать дневные показатели. Load — загрузить результат в аналитическое хранилище.
Слово «ETL» используют в двух смыслах. В узком — это именно порядок шагов, где преобразование идёт до загрузки. В широком — любой конвейер данных: «ETL упал ночью» обычно значит «данные не доехали», как бы ни был устроен процесс внутри.
| Этап | Что происходит | Пример |
|---|---|---|
| Extract | Забираем новые данные из источников | события приложения за прошедший час, оплаты из биллинга за день |
| Transform | Чистим, приводим к единому виду, соединяем | время в одном часовом поясе, дубли событий удалены, у оплаты есть канал пользователя |
| Load | Записываем в хранилище | таблицы events, payments, дневная витрина с DAU и выручкой |
| Потребление | Читаем готовые таблицы | дашборд, отчёт для продакта, запрос аналитика |
Чем ETL отличается от ELT
В ELT шаги переставлены: extract, load, transform. Сырые данные загружают в хранилище почти как есть, а чистят и соединяют уже внутри него. Преобразования пишут как SQL-модели: одна модель читает сырые события и убирает дубли, следующая строит из неё дневные показатели. Так работает, например, dbt.
ELT стал распространённым вместе с облачными хранилищами. Когда хранилище умеет быстро считать большие объёмы и его мощность можно нарастить, выгоднее один раз загрузить сырые данные и пересчитывать из них всё остальное. Если в логике нашли ошибку, модель исправляют и пересобирают витрины из сырья, не выгружая данные из источников заново.
У классического ETL остаются свои причины. Если в сырых данных есть то, что нельзя хранить в аналитике, например лишние персональные данные, их вычищают до загрузки. Если данных очень много, а нужна небольшая агрегированная часть, возить всё сырьё бессмысленно. На практике многие конвейеры смешанные: лёгкая очистка на входе, основная логика в хранилище.
| Что сравниваем | ETL | ELT |
|---|---|---|
| Порядок | извлечь → преобразовать → загрузить | извлечь → загрузить → преобразовать |
| Где идёт преобразование | вне хранилища, в отдельном процессе | внутри хранилища, обычно на SQL |
| Сырые данные в хранилище | часто нет, только результат | да, их можно пересчитать заново |
| Исправить ошибку в логике | перевыгрузить данные из источника | пересобрать модели из уже загруженного сырья |
| Когда уместен | чувствительные данные, большие объёмы при малом нужном срезе | аналитическое хранилище с запасом мощности, частые изменения логики |
Пакетная загрузка и потоковая: когда данные нужны сразу
Пакетная загрузка (batch) запускается по расписанию: раз в сутки, раз в час. Она проще в поддержке и подходит для большинства продуктовых отчётов: дневной DAU, недельный retention, выручка за месяц.
Потоковая обработка (streaming) обрабатывает события по мере поступления, с задержкой в секунды. Она нужна там, где решение принимают сразу: антифрод, мониторинг ошибок оплаты, рекомендации в текущей сессии. Цена — сложнее инфраструктура и сложнее исправлять ошибки задним числом.
Практический вопрос звучит так: что изменится, если цифра появится через час, а не через минуту? Если ничего, пакетной загрузки достаточно.
Инкрементальная загрузка: водяной знак и повторные запуски
Перезаливать всю историю при каждом запуске дорого, поэтому конвейер запоминает, докуда он дочитал. Эту отметку называют водяным знаком (watermark): чаще всего это максимальное updated_at или время события, которое уже загружено. Следующий запуск забирает только записи новее отметки.
В учебной базе SQL-курса последнее событие за 29 августа случилось в 22:59. Если прошлый запуск остановился на отметке 23:00, следующий заберёт 226 событий — всё, что пришло 30 августа до закрытия выгрузки. Запрос ниже показывает, что произойдёт, если этот запуск по ошибке повторить и дописать пачку ещё раз.
with batch as (
-- всё, что новее отметки прошлого запуска
select *
from events
where event_time > timestamp '2026-08-29 23:00'
),
loaded_twice as (
-- запуск повторили, и пачку дописали второй раз
select * from batch
union all
select * from batch
)
select
count(*) as rows_loaded,
count(distinct event_id) as unique_events
from loaded_twice;DAU по distinct user_id такой дубль не заметит, а число событий, выручка и конверсии «на событие» удвоятся за этот день. Поэтому загрузку делают идемпотентной: повторный запуск за тот же период даёт тот же результат, а не добавляет строки.
Что такое идемпотентность и как её добиваются
Идемпотентная загрузка — такая, которую можно безопасно запустить второй раз. Это нужно постоянно: задание упало на середине, источник прислал исправленные данные, в логике нашли ошибку и пересчитывают неделю.
Два самых частых приёма. Первый — перезапись периода: удалить из целевой таблицы все строки за день и вставить их заново. Второй — слияние по ключу (merge или insert ... on conflict): строка с уже известным event_id обновляется, а не дублируется. Оба требуют стабильного ключа. Если у события нет надёжного идентификатора, дубли приходится ловить по логическому ключу, например по пользователю, названию события и времени.
Отдельная ловушка — опоздавшие данные. Мобильное приложение может отправить событие через час после того, как оно случилось, и его event_time окажется раньше водяного знака. Конвейер, который читает строго «новее отметки», такое событие потеряет. Поэтому окно чтения обычно берут с запасом, например перечитывают последние сутки, а дубли убирают по ключу.
Какие проверки качества данных ставят после загрузки
Конвейер может отработать без ошибок и при этом привезти неправильные данные. Поэтому после загрузки запускают проверки. Первая — свежесть: до какого момента дошла каждая таблица.
| source | last_record |
|---|---|
| users | 2026-08-29 |
| events | 2026-08-30 12:59:00 |
| payments | 2026-08-30 |
| subscriptions | 2026-08-30 |
select 'users' as source, max(signup_date)::varchar as last_record from users
union all
select 'events', max(event_time)::varchar from events
union all
select 'payments', max(paid_at)::varchar from payments
union all
select 'subscriptions', max(started_at)::varchar from subscriptions;Объём, дубли и пустые поля: ещё две проверки
Вторая проверка — объём. Сравните число строк за день с обычным уровнем: если пришло в три раза меньше, почти наверняка что-то не догрузилось. Здесь базой служит среднее за семь предыдущих дней.
| day | events | avg_prev_7 | pct_of_baseline |
|---|---|---|---|
| 2026-08-26 | 672 | 586 | 114,7 |
| 2026-08-27 | 655 | 592 | 110,7 |
| 2026-08-28 | 690 | 600 | 114,9 |
| 2026-08-29 | 588 | 610 | 96,4 |
| 2026-08-30 | 226 | 619 | 36,5 |
with daily as (
select event_time::date as day, count(*) as events
from events
group by 1
),
with_baseline as (
select
day,
events,
avg(events) over (order by day rows between 7 preceding and 1 preceding) as avg_prev_7
from daily
)
select
day,
events,
round(avg_prev_7) as avg_prev_7,
round(100.0 * events / avg_prev_7, 1) as pct_of_baseline
from with_baseline
where day >= date '2026-08-26'
order by day;Набор правил в одном запросе
Третья группа проверок — правила, которые данные не должны нарушать никогда: логических дублей нет, обязательные поля заполнены, у каждой оплаты есть пользователь, суммы положительные. Удобно собрать их в один запрос, где каждая строка — правило и число нарушений.
На учебной базе пять правил из шести проходят. У 55 пользователей из 4613 не заполнена страна. Это не обязательно ошибка конвейера: страну могли не определить при регистрации. Но теперь это известно заранее, и разрез по странам не удивит пустой строкой в отчёте.
| check_name | problem_rows | status |
|---|---|---|
| events: дубли логических событий | 0 | ok |
| events: пустой user_id или event_time | 0 | ok |
| events: событие раньше регистрации | 0 | ok |
| payments: оплата без пользователя | 0 | ok |
| payments: сумма не больше нуля | 0 | ok |
| users: пустая страна | 55 | проверить |
with checks as (
select 'events: дубли логических событий' as check_name,
count(*) - count(distinct (user_id, event_name, event_time)) as problem_rows
from events
union all
select 'events: пустой user_id или event_time',
count(*) filter (where user_id is null or event_time is null)
from events
union all
select 'events: событие раньше регистрации', count(*)
from events as e
join users as u on u.user_id = e.user_id
where e.event_time::date < u.signup_date
union all
select 'payments: оплата без пользователя', count(*)
from payments as p
left join users as u on u.user_id = p.user_id
where u.user_id is null
union all
select 'payments: сумма не больше нуля', count(*) filter (where amount <= 0)
from payments
union all
select 'users: пустая страна', count(*) filter (where country is null)
from users
)
select check_name, problem_rows,
case when problem_rows = 0 then 'ok' else 'проверить' end as status
from checks;Где аналитик встречается с ETL: сломанный дашборд
Вернёмся к воскресенью 30 августа. На графике DAU выглядит как обвал: 530 в субботу, 223 в воскресенье. Проверка объёма уже подсказала причину: событий за день пришло 36,5% от обычного уровня. Последнее событие — 12:59.
Чтобы убедиться, сравните одинаковые окна. В воскресенье 23 августа до 13:00 было 249 событий и 231 активный пользователь, 30 августа за то же время — 226 событий и 223 пользователя. Половина дня идёт вровень с прошлой неделей, обвала нет, есть недогруженный день.
Вторая деталь видна в проверке свежести. Регистрации в таблице users заканчиваются 29 августа, а события и оплаты есть и за 30-е. Дашборд, который соединит эти таблицы по дате, покажет за 30 августа ноль регистраций. По одной базе нельзя отличить «никто не зарегистрировался» от «регистрации ещё не доехали»: об этом говорит только время последней загрузки каждого источника.
Учебная база SQL-курса, DuckDB. * 30 августа — неполный день: выгрузка закрыта в 13:00.
- Цифра упала только за последний день — проверьте свежесть таблицы и время последнего события.
- Цифра удвоилась за один день — ищите повторную загрузку и дубли по ключу.
- Одна метрика обнулилась, остальные живы — источник этой метрики не обновился или сменилось название события.
- Цифры за прошлые дни изменились задним числом — конвейер пересчитал период или догрузил опоздавшие данные. Это нормально, если об этом знают.
Какие инструменты используют для ETL
Инструменты делятся по задачам, а не по буквам в аббревиатуре. Оркестратор запускает шаги по расписанию, следит за зависимостями и повторяет упавшие задания: самый известный из открытых — Apache Airflow. Коннекторы забирают данные из баз и сервисов в хранилище. Инструменты преобразований вроде dbt превращают SQL-запросы в модели с зависимостями, тестами и документацией. Для очень больших объёмов используют распределённые движки, например Spark, для потоков — брокеры сообщений вроде Kafka.
Для небольшой команды всё это может заменить набор SQL-скриптов по расписанию. Инструмент важен меньше, чем три привычки: загрузка идемпотентна, проверки запускаются после каждого прогона, а у каждой таблицы известны источник и время последнего обновления.
Что читать дальше
Куда ETL загружает данные и как из них собирают витрины, разобрано в соседнем материале. Проверки и дедупликацию можно отработать на той же учебной базе.
Материалы по теме

DWH, витрина данных и data lake: что это и чем они отличаются
DWH — хранилище данных для анализа, витрина — готовая таблица под конкретную задачу, data lake — хранилище сырых файлов. Чем DWH отличается от базы приложения, из каких слоёв состоит, как выглядит витрина на SQL и почему в двух витринах бывают разные цифры.

OLAP и OLTP: в чём разница простыми словами
OLTP — системы, которые обслуживают работу продукта мелкими транзакциями, OLAP — системы для тяжёлых аналитических запросов. Чем они отличаются по запросам, схеме и хранению, почему отчёты не строят на рабочей базе и что такое OLAP-куб.
Дубликаты в SQL: как найти и убрать дубли событий, не потеряв реальные действия
Как отличить повторную доставку события от повторного действия пользователя, выбрать ключ дедупликации и окно, оставить одну строку через ROW_NUMBER и проверить, что после очистки не исчезли настоящие заказы.