Перейти к содержимому
TelegramGitHub

Fundamentals of Data Engineering, глава 8: Запросы, моделирование, трансформация

Главная мысль: на этом этапе сырые данные становятся полезными. Запрос читает данные. Модель задаёт структуру и бизнес-логику. Трансформация сохраняет результат для следующих шагов.

Жизнь SQL-запроса от compile до результата и способы улучшить производительность

Query читает данные (R в CRUD) или меняет их. В главе в основном речь об OLAP-системах и SQL. Термин database здесь значит движок запросов вместе с хранилищем: warehouse или Spark над S3.

Группа SQLЧто делаетПримеры
DDLСоздаёт и удаляет объектыCREATE, DROP
DMLРаботает с даннымиSELECT, INSERT, UPDATE, DELETE, MERGE
DCLУправляет доступомGRANT, REVOKE
TCLУправляет транзакциямиCOMMIT, ROLLBACK
  • База проверяет код: синтаксис, объекты, права.
  • SQL превращается в bytecode.
  • Query optimizer выбирает порядок шагов. Он учитывает join, индексы и объём scan.
  • Запрос выполняется, результат возвращается.

Optimizer мы не настраиваем напрямую. Но нужно уметь читать explain plan.

ПриёмСуть
Join и схемаЕсли одни и те же join повторяются, сделайте prejoin или широкую таблицу
Row explosionПовторы в join-ключах дают произведение строк (5 × 10 = 50) и могут уронить запрос
CTEЧитаются лучше, чем вложенные subquery
EXPLAINСмотрите план, а также диск, память, сеть, shuffle, конкуренцию запросов
PruningНе делайте SELECT *. В columnar берите нужные колонки, используйте partition и cluster key. В row-БД нужны индексы
CommitsЗнайте модель транзакций вашей БД: ACID, dirty read, snapshot
VacuumУдаляйте старые версии записей: меньше bloat, точнее планы
Query cacheПовторный запрос может отработать за секунду вместо минуты

Три примера commit-моделей:

  • PostgreSQL: ACID, блокировки строк. Плохо подходит для больших analytics scan.
  • BigQuery (похоже на Snowflake и Spark): читает последний snapshot, одна запись за раз, без блокировок на чтение.
  • MongoDB: настраиваемая согласованность. В некоторых режимах при перегрузке молча теряет записи.

Правило: выбирайте БД и настройки под задачу. Плохих баз нет, есть неверное применение.

Production-БД не стоит нагружать analytics scan. Поэтому появился шаблон fast follower: CDC копирует данные в OLAP-базу, запросы идут туда. Druid и BigQuery сочетают streaming buffer и columnar storage.

Ограничение: это те же SELECT по текущему состоянию. Мы не реагируем на события в потоке.

Kappa architecture хранит всё как события. Retention может быть месяцы или годы. Запрашивать можно прямо из streaming storage (KSQL) или через Spark и Flink.

Типы окон в streaming-запросах: fixed, sliding, session, watermark

В streaming расчёт запускают сами данные, а не cron. Для этого нужны окна (windows):

ОкноКак работает
SessionГруппирует события одного ключа, закрывается после паузы (например 5 минут)
Fixed (tumbling)Окна фиксированной длины идут подряд без пересечений
SlidingОкна одной длины пересекаются (60 с каждые 30 с)
WatermarkПорог времени. Событие старше порога считается late-arriving

Streaming можно объединять с другими данными тремя способами:

  • Join двух таблиц, которые питает stream.
  • Enrichment: добавляем к событиям данные из кэша, таблицы или файла.
  • Stream-to-stream join: нужен buffer. События ждут пару в течение retention interval, потом вытесняются.

Data model показывает, как данные связаны с реальным бизнесом. Без модели получаются data swamp и разные определения одного и того же. Например, кто такой customer: купил за 30 дней или за год?

Три уровня модели:

УровеньЧто описывает
ConceptualБизнес-сущности и правила. Удобна ER-диаграмма
LogicalТипы полей, primary и foreign key
PhysicalКонкретные базы, схемы, таблицы, настройки

Модель делают вместе с бизнесом.

Grain (зерно) это уровень детализации строки. Храните данные на самом мелком grain. Агрегировать легко, восстановить детали нельзя.

Цель: убрать дублирование и обеспечить referential integrity.

ФормаТребование
DenormalizedДопускаются вложенные и повторяющиеся данные
1NFНет повторов и вложенности в колонках, есть уникальный primary key
2NF1NF и нет partial dependency на часть составного ключа
3NF2NF и нет transitive dependency: неключевое поле не зависит от другого неключевого

В OLAP денормализация часто нормальна. Степень нормализации зависит от задачи.

Сравнение подходов Inmon, Kimball, Data Vault и wide table с компромиссами

Подход 1989 года. Warehouse отделяет аналитику от источников. Он subject-oriented, integrated, nonvolatile и time-variant. Данные приходят через ETL в нормализованную 3NF-модель, это единый источник правды. Потом из него строят data marts для отделов, часто со star schema.

Подход снизу вверх. Витрина и есть warehouse. Два вида таблиц:

ТаблицаСвойства
FactЧисла и события. Узкая и длинная, append-only, на самом мелком grain. Ссылается только на dimensions
DimensionКонтекст: кто, что, где, когда. Широкая и короткая, денормализована

Star schema это одна fact-таблица, окружённая dimensions. Меньше join, проще для бизнеса. Conformed dimension общий для нескольких star schema. Он позволяет объединять факты.

Slowly changing dimension (SCD) хранит изменения атрибутов:

ТипЧто делает
Type 1Перезаписывает. История теряется. Это поведение по умолчанию
Type 2Новая строка при изменении, с датами действия. Самый частый на практике
Type 3Новая колонка для старого значения

Kimball подходит только для batch. На streaming его перенести трудно.

Метод Dan Linstedt. Структура отделена от атрибутов. Данные грузятся из источников insert-only. Нет понятий хороших или conformed данных: бизнес-логика применяется при чтении.

ТаблицаЧто хранит
HubУникальные business keys. Поля: hash key, load date, record source, business key
LinkСвязи между hubs, many-to-many
SatelliteАтрибуты и контекст hub или link

Новая связь в бизнесе это новый link. Часто Data Vault служит слоем загрузки, а сверху строят star schema. Есть также PIT и bridge таблицы.

Одна очень широкая денормализованная таблица, обычно в columnar БД. Это работает по причинам:

  • Хранение дешёвое.
  • В columnar пустые значения почти ничего не стоят, читаются только нужные колонки.
  • Добавить колонку это правка metadata.
  • Нет join, scan быстрее.

Минусы: теряется бизнес-логика, обновление элементов массива дорого. Подходит, когда строгая модель не нужна или данных очень много.

Можно запрашивать источники напрямую. Для старта это допустимо. Риски: результаты несогласованны, определения могут быть неверными, источник нагружается. Потом всё равно придётся строить модель.

Единого подхода нет. Batch-методы плохо ложатся на поток: непонятно, как вести SCD Type 2 непрерывно. Схема события может поменяться без предупреждения. Эксперты советуют ждать изменений и держать гибкую схему. Некоторые называют Data Vault вариантом. Авторы ждут новых подходов с metrics и semantic layer.

Query только читает. Transformation сохраняет результат, временно или постоянно. Трансформации сложнее: много источников, повторное использование промежуточных результатов. Они опираются на orchestration.

Distributed joins: логический join делится на join на узлах кластера.

ТипКогдаСтоимость
Broadcast joinОдна таблица мала и помещается на узел. Её рассылают всем узламДёшево
Shuffle hash joinОбе таблицы велики. Данные перераспределяют по хешу ключаДорого

Фильтруйте заранее, чтобы получить broadcast join.

ETL и ELT. ETL появился из-за ограничений источников и приёмников. ELT загружает сырые данные и преобразует их в warehouse. ELT без плана в data lake ведёт к data swamp. Авторы советуют применять термины к отдельным pipeline, а не ко всей организации.

SQL или код. SQL декларативен, но подходит для сложных DAG через CTE, скрипты и orchestration. Три вопроса перед отказом от SQL:

  • Насколько сложно это записать в SQL?
  • Будет ли SQL читаемым?
  • Нужна ли библиотека для повторного использования?

Советы для Spark: фильтруйте рано, используйте нативный API, осторожно с UDF (Python UDF медленные), смешивайте с SQL ради Catalyst optimizer. dbt помогает повторно использовать SQL.

Шаблоны обновления данных: truncate, insert only, delete, upsert и merge

Update patterns:

ШаблонСуть
Truncate and reloadОчистить таблицу и загрузить заново
Insert onlyДобавлять новые версии. Текущее состояние даёт query, view или materialized view
DeleteHard delete, soft delete (флаг) или insert deletion (новая запись с флагом)
Upsert / mergeОбновить по ключу или вставить. Merge ещё и удаляет

Важно:

  • Columnar-системы не гарантируют уникальность ключа. Ключом управляет инженер.
  • Файловые системы используют copy on write (COW): изменённый файл переписывается.
  • Не вставляйте по одной строке в columnar. Грузите batch или micro-batch.
  • Не делайте near real-time merge из CDC. Лучше merge раз в час.
  • Merge больших наборов быстрый, но одной записи дорогой.

Schema updates. Колонки в columnar добавлять легко. Сложнее организация: авто-обновление схемы (как в Fivetran) может сломать downstream. Не используйте SELECT *. Для JSON храните сырой JSON и рядом плоские поля для частых данных.

Data wrangling превращает грязные данные в чистые. Его инструменты это IDE для плохих данных. Они показывают профиль, ошибки, пропуски. Тяжёлые задачи уходят в Spark.

Business logic и derived data. Метрики вроде прибыли сложны: фрод, отмены, attribution маркетинга. Логика в ETL-скриптах нарушает DRY. Альтернатива это metrics layer: логика в одном месте, запросы генерируются из метрик.

MapReduce определил batch-обработку эпохи big data: map по блокам, shuffle по ключам, reduce агрегирует. Map масштабируется почти линейно. Минус: всё пишется на диск. После MapReduce (Spark, BigQuery) данные кэшируются в памяти, на диск идёт только spill. В облаке память можно арендовать на время задачи.

ОбъектСуть
ViewСохранённый запрос. Нужен для безопасности, deduplicated представления, частых join
Materialized viewРезультат считается заранее. Может ускорять и запросы, которые на него не ссылаются
Composable materialized viewsЦепочки таблиц, которые обновляются по мере поступления данных (Databricks live tables)
Federated queryOLAP-БД читает внешний источник: object storage, MySQL, PostgreSQL
Data virtualizationДвижок без своего хранилища (Trino, Presto). Важен query pushdown: фильтры идут в источник

Virtualization не защищает production-БД: Trino читает её при каждом запросе. Лучше выгружать данные раз в день в S3. Это подходит и для data mesh.

Streaming query показывает текущее состояние. Streaming transformation готовит данные для потребителя, например обогащает IoT-события метаданными устройства. Граница между query и transformation размыта.

  • Streaming DAG: слияние и обогащение потоков в реальном времени. В Pulsar DAG это базовая абстракция.
  • Micro-batch (Spark Streaming) обрабатывает пачки каждые 1 с или 2 минуты. True streaming (Flink, Beam) обрабатывает по одному событию, с большими накладными расходами.
  • Выбор зависит от задачи. Метрики Black Friday раз в минуты подойдут micro-batch. Обнаружение DDoS раз в секунду требует true streaming.
  • Не верьте benchmark вендоров. Тестируйте на своих данных.
КтоЧто важно
UpstreamБизнес-определения и владельцы источников. Просите уведомлять об изменениях схемы. Не перегружайте источники
DownstreamАналитики, data scientists, ML-инженеры, бизнес. Им нужны быстрые и надёжные данные
UndercurrentЧто делать
SecurityКонтроль доступа к колонкам и строкам, секреты не в коде, шифрование
Data managementЕдиные имена, каталог, data lineage, MDM, удаление по запросу, маскирование
DataOpsТесты качества на входе и выходе, мониторинг очередей, памяти, concurrency, FinOps
Data architectureНе используйте RDBMS или Elasticsearch как warehouse
OrchestrationЗависимости вместо cron
Software engineeringЧистый код, меньше UDF. dbt и CI/CD. Понимайте SQL, который генерирует low-code
Термин (EN)Что значит
Query optimizerЧасть БД, выбирающая самый дешёвый план запроса
Explain planПлан выполнения запроса. Показывает шаги и стоимость
Row explosionРост числа строк из-за повторов в ключах join
PruningЧтение только нужных колонок и партиций
VacuumУдаление устаревших версий записей
WindowОкно событий в streaming (fixed, sliding, session)
WatermarkПорог, после которого событие считается поздним
GrainУровень детализации строки в таблице
NormalizationСнижение дублирования данных по normal forms
Fact / dimensionСобытие с числами и контекст к нему в Kimball
SCDSlowly changing dimension: способ хранить историю атрибутов
Data VaultМодель из hubs, links и satellites
Broadcast / shuffle hash joinДва способа распределённого join
Upsert / mergeОбновить по ключу или вставить, при merge ещё удалить
Copy on writeИзменение файла через его полную перезапись
Materialized viewView с заранее посчитанным результатом
Data virtualizationЗапросы к внешним источникам без своего хранилища
Metrics layerСлой, где определены бизнес-метрики
  • Читайте EXPLAIN до оптимизации. Сначала уберите лишний scan и повторные join.
  • Храните факты на самом мелком grain. Агрегаты делайте в витринах.
  • Выберите модель под задачу. Для отчётов подходит star schema, для гибкой загрузки Data Vault, для больших nested данных wide table.
  • В columnar БД не делайте построчные insert и частые merge. Батчи и разумная частота спасают очередь.
  • Определения метрик держите в одном месте: metrics layer или dbt. Не копируйте логику по ETL-скриптам.
  • Ставьте тесты качества на входе и выходе трансформации. Следите за стоимостью запросов.
  • Не нагружайте production-БД аналитикой. Используйте CDC и отдельную OLAP-базу.
  1. Что делает query optimizer и как вам помогает explain plan?
  2. Чем fact table отличается от dimension table и зачем нужен SCD Type 2?
  3. Когда broadcast join лучше shuffle hash join?
  4. Почему merge из CDC в реальном времени плохо работает в columnar warehouse?

Предыдущая: Приём данных. Следующая: Выдача данных.