Проектирование пайплайна: от сырых файлов к DuckLake
Этот модуль — сквозной практический проект. На протяжении семи уроков мы построим один конкретный аналитический lakehouse целиком на DuckDB: спроектируем архитектуру, напишем ELT-слои, обработаем датасет больше оперативной памяти, сделаем партиционированные витрины с инкрементальными апдейтами, забенчмаркаем результат против Pandas и Spark, опубликуем витрину для браузера через DuckDB-WASM и в финале разберём архитектуру и проведём code review.
Каждый предыдущий модуль курса дал кусок этой картины: friendly SQL, чтение внешних данных, larger-than-memory исполнение, запись данных, DuckLake. Капстоун собирает их в одно работающее решение. Этот первый урок — про проектирование: какую задачу решаем, из каких слоёв состоит пайплайн и почему каждый слой именно такой.
Постановка задачи и датасет
Возьмём реалистичную задачу аналитической инженерии. Есть оператор городских такси-поездок. Сырые данные приходят дампами на объектное хранилище: исторические поездки — помесячными файлами Parquet (по файлу на месяц, формат стабильный), а справочник зон города — отдельным CSV (обновляется редко, формат «грязноватый»: смешанные регистры, лишние пробелы). Суммарный объём исторических поездок — несколько десятков гигабайт, заведомо больше RAM типичного ноутбука.
Аналитикам нужны витрины: выручка и число поездок по дням и зонам, средняя длительность и дистанция поездки, разбивка по способу оплаты. Витрины должны обновляться при появлении нового месячного файла — без полного пересчёта истории.
Сырые данные на объектном хранилище (s3:// или MinIO/локальная папка):
raw/trips/year=2024/month=01/trips.parquet
raw/trips/year=2024/month=02/trips.parquet
...
raw/trips/year=2026/month=04/trips.parquet -- ~28 месячных файлов
raw/zones/zones.csv -- справочник зон, ~260 строк
Это типичная стартовая позиция: данные уже где-то лежат, форматы разные, объём не помещается в память, и нужен повторяемый пайплайн, а не разовый скрипт.
Почему DuckDB и DuckLake для этой задачи
Прежде чем рисовать слои, зафиксируем выбор инструментов — он определяет архитектуру.
DuckDB как движок. Он читает Parquet и CSV напрямую с объектного хранилища без отдельного шага загрузки, исполняет larger-than-memory агрегации со спиллом на диск, и весь пайплайн — это один процесс без сервера и кластера. Для «локального аналитического lakehouse» это точное попадание: не нужно поднимать Spark-кластер ради десятков гигабайт.
DuckLake как формат хранения витрин. Сырые файлы — это просто файлы, у них нет ни схемы-как-контракта, ни версий, ни ACID. Витрины же должны обновляться инкрементально и безопасно. DuckLake даёт лейкхаус-слой: данные в Parquet на хранилище, метаданные в SQL-каталоге, кросс-табличный ACID, снапшоты с time-travel, инкрементальные MERGE. Для слоя, который аналитики читают и который меняется новыми загрузками, это правильная основа.
Граница ответственности: DuckDB — это движок (исполняет SQL, читает файлы, считает), DuckLake — это формат хранения (организует Parquet-файлы и метаданные в таблицы с ACID). Они не конкурируют: пайплайн читает движком DuckDB, а результат складывает в формат DuckLake. В капстоуне каталог DuckLake возьмём в SQLite — проект локальный и однопользовательский; для команды это был бы PostgreSQL.
Слои пайплайна: raw, staging, marts
Пайплайн строится по классической многослойной схеме. Каждый слой решает одну задачу и подаёт результат следующему.
Слой raw — сырые файлы, которые мы не трогаем. Принцип: исходные данные иммутабельны, пайплайн только читает их. Если что-то пошло не так, всегда можно перечитать raw заново. DuckDB обращается к raw напрямую — read_parquet и read_csv по путям с globs.
Слой staging — очистка «один к одному с источником». На каждый источник — один staging-объект: stg_trips для поездок, stg_zones для зон. Здесь и только здесь чинятся болячки источника: грязный CSV приводится в порядок, типы кастуются явно (строка-дата в DATE, текстовая сумма в DECIMAL), колонки переименовываются в единый snake_case. Бизнес-логики на этом слое нет — задача staging сделать данные чистыми и предсказуемыми, не более.
Слой marts — бизнес-витрины. Здесь происходит моделирование: соединение поездок со справочником зон, агрегация по дням и зонам, расчёт метрик. Это то, что увидят аналитики, и именно этот слой хранится в DuckLake — с ACID, снапшотами и инкрементальными апдейтами.
Зачем разделять staging и marts, а не делать всё одним запросом? Разделение даёт три вещи. Изоляция изменений источника: если поставщик переименовал колонку в Parquet, правка нужна только в stg_trips, marts не трогаются. Переиспользование: один stg_trips питает несколько витрин. Тестируемость: чистоту данных проверяем на staging, бизнес-корректность — на marts, и при сбое сразу видно, какой слой виноват.
Поток данных в проекте
Сведём архитектуру в конкретный поток, который реализуем в следующих уроках.
Распределение уроков модуля по этому потоку:
| Урок | Что строим | Слой |
|---|---|---|
| 1 (этот) | Архитектура и проектирование | весь пайплайн |
| 2 | ELT: staging, очистка, моделирование | raw -> staging -> marts |
| 3 | Out-of-core агрегация датасета больше RAM | тяжёлый расчёт в marts |
| 4 | Партиционированные витрины и MERGE | marts в DuckLake |
| 5 | Бенчмаркинг против Pandas/Spark | проверка marts-расчёта |
| 6 | Публикация витрины через DuckDB-WASM | подача |
| 7 | Разбор архитектуры и code review | весь проект |
Готовим окружение. Понадобится DuckDB 1.5.x, расширения httpfs (для объектного хранилища) и ducklake (для лейкхаус-слоя):
-- Расширения для капстоуна
INSTALL httpfs; LOAD httpfs;
INSTALL ducklake; LOAD ducklake;
-- Лейкхаус для marts-слоя: каталог в SQLite, данные в папке
ATTACH 'ducklake:sqlite:capstone_catalog.sqlite' AS lake
(DATA_PATH 'capstone_lake/');
-- Проверка
SELECT current_setting('memory_limit');
Если данные на S3 или совместимом хранилище (MinIO, R2), понадобится ещё secret с доступом — его настроим в следующем уроке вместе с чтением raw.
Принципы, которым следует проект
Зафиксируем правила, которые будут видны во всех последующих уроках.
- Raw иммутабелен. Пайплайн только читает сырые файлы. Любое исправление — это код в staging, а не правка исходника.
- Каждый слой — одна ответственность. Staging чистит, marts моделирует. Не смешиваем.
- Идемпотентность. Повторный запуск пайплайна на тех же данных даёт тот же результат. Это требование к тому, как пишутся загрузки (увидим в уроке про
MERGE). - Инкрементальность с самого начала. Витрины проектируются так, чтобы новый месячный файл добавлялся без пересчёта истории, — это закладывается в архитектуру партиционирования, а не прикручивается потом.
- Контроль ресурсов явный. Раз датасет больше RAM,
memory_limitиtemp_directoryзадаются осознанно, а не оставляются по умолчанию.
Эти принципы — не бюрократия, а то, что отличает повторяемый production-пайплайн от разового скрипта. Дальше каждый урок будет реализовывать конкретный слой этой архитектуры.
Попробуй сам
Цель этого задания — подготовить и осмыслить проект, кода-расчётов здесь ещё нет.
Задания:
- Установите DuckDB 1.5.x. Выполните
INSTALL httpfs; INSTALL ducklake;и подключите локальный DuckLake-лейкхаус командойATTACHиз урока. Убедитесь, что каталог-файл и папка данных создались. - Нарисуйте на бумаге слои пайплайна (raw, staging, marts, consumers) и для каждого подпишите: какую одну задачу он решает и что будет, если его убрать.
- Сформулируйте письменно, почему справочник зон (грязный CSV) и поездки (стабильный Parquet) проходят через отдельные staging-объекты, а не через один.
- Объясните себе, почему именно слой marts хранится в DuckLake, а слой raw — нет. Какие свойства DuckLake (ACID, снапшоты, MERGE) нужны marts и не нужны сырым файлам.