Разбор архитектуры решения и code review
Капстоун-проект построен: сырые файлы прошли staging, превратились в партиционированную витрину в DuckLake, обновляются инкрементально через MERGE и публикуются для браузера через DuckDB-WASM. Финальный урок — рефлексия. Разберём принятые архитектурные решения и проверим, почему каждое из них правильное; пройдёмся по типичным ошибкам, которые легко допустить в таком пайплайне; и сведём всё в чеклист готовности.
Этот урок не добавляет новых механизмов — он учит смотреть на готовое решение глазами ревьюера. Умение оценить чужой (и свой) пайплайн — отдельный навык аналитического инженера.
Разбор архитектурных решений
Пройдём по ключевым решениям проекта и для каждого зафиксируем, какую альтернативу мы отвергли и почему.
Трёхслойность. Альтернатива — один гигантский запрос от сырья сразу к витрине. Отвергнута: монолит теряет изоляцию (правка схемы источника задевает весь запрос), переиспользование (staging нельзя подключить к другой витрине) и диагностику (при сбое неясно, данные виноваты или бизнес-логика). Цена слоёв — чуть больше кода; выгода — поддерживаемость.
DuckDB как движок. Альтернатива — Spark. Отвергнута для масштаба капстоуна: десятки гигабайт на одной машине не требуют распределённого кластера, а Spark принёс бы JVM, кластерный планировщик и инфраструктуру, которые здесь чистый overhead. Бенчмарк это подтвердил измерением.
DuckLake для marts. Альтернатива — материализовать витрину обычными Parquet-файлами. Отвергнута: витрине нужны ACID (атомарность обновления), снапшоты (откат неудачной загрузки, воспроизводимость) и инкрементальный MERGE — у голых файлов этого нет. При этом raw мы намеренно оставили обычными файлами: сырью эти свойства не нужны.
Партиционирование плюс MERGE. Альтернатива — полный пересчёт витрины при каждом новом файле. Отвергнута: пересчитывать 41 миллион строк ради одного месяца расточительно. Партиционирование по месяцам дало локальность обновления, MERGE дал идемпотентный инкрементальный апдейт.
Общий принцип, видимый во всех четырёх решениях: каждое выбрано под конкретный масштаб и характер задачи, а не по моде. Это и есть то, что проверяет ревьюер — не «модный ли стек», а «соответствует ли стек задаче».
Типичные ошибки в коде пайплайна
Теперь — code review. Пройдёмся по ошибкам, которые легко допустить в DuckDB-пайплайне такого типа. Каждая — реальная, и в нашем проекте её нет именно потому, что о ней знали.
Деньги в DOUBLE вместо DECIMAL. Сумма поездки в DOUBLE — это накопление ошибки округления плавающей точки на миллионах строк агрегации. В капстоуне total_amount объявлен DECIMAL — для денег это обязательно.
SELECT * в staging. Соблазн написать SELECT * FROM read_parquet(...) в staging вместо явного списка колонок. Ошибка: при появлении новой колонки в источнике она молча протечёт через все слои, а типы останутся теми, что угадал источник. Явный список колонок с явными кастами — это контракт.
Инкрементальный шаг на INSERT вместо MERGE. INSERT нового месяца кажется проще MERGE. Но INSERT не идемпотентен: повторный запуск (упал на полпути, перезапустили) задублирует строки. MERGE при повторе перезаписывает совпавшее и не плодит дубли.
Бизнес-логика в staging. Посчитать выручку прямо в stg_trips кажется экономией слоя. Но это ломает границу: staging перестаёт быть переиспользуемым и тестируемым, а при сбое неясно, чья вина.
Игнорирование memory_limit на больших данных. Полагаться на умолчания, когда датасет больше RAM, — рецепт либо неожиданного спилла, либо медленного запроса. Лимит и temp_directory задаются явно.
Публикация сырья в браузер. Отдать DuckDB-WASM витрину целиком или сырые поездки — перегрузить вкладку. В браузер идёт компактный предагрегированный срез.
Самая коварная из этих ошибок — INSERT вместо MERGE в инкрементальном шаге, потому что она не проявляется на счастливом пути. Пайплайн работает месяцами, пока однажды шаг не упадёт на полпути и его не перезапустят — и тогда витрина тихо получает дубли, а ошибка в данных всплывает гораздо позже. Идемпотентность не опция, а требование к любому шагу, который может быть перезапущен.
Чеклист code review пайплайна
Сведём проверку в чеклист, применимый к любому DuckDB-пайплайну такого типа.
| Категория | Что проверить |
|---|---|
| Слои | Raw иммутабелен; staging чистит без бизнес-логики; marts моделирует. Границы не размыты. |
| Типы | Деньги в DECIMAL, не DOUBLE. Даты в DATE/TIMESTAMP. Касты явные, не на усмотрение источника. |
| Staging | Явный список колонок, не SELECT *. Мусор отсеян в WHERE. Единый нейминг. |
| Инкрементальность | Новый файл не требует пересчёта истории. Обновление через MERGE, идемпотентно. |
| Партиционирование | Ключ партиции соответствует тому, как данные приходят и читаются. |
| Память | memory_limit и temp_directory заданы явно; temp_directory на быстром диске. |
| Хранение | Витрины с ACID-потребностями в DuckLake; сырьё — обычные файлы. |
| Публикация | В браузер идёт компактный срез, а не вся витрина и не сырьё. |
| Идемпотентность | Повторный запуск любого шага даёт тот же результат. |
Этот чеклист — не формальность. Каждый пункт соответствует ошибке, которая в реальном пайплайне рано или поздно стреляет.
Куда расти от капстоуна
Проект сделан, но в production его можно усилить. Это направления, в которые капстоун естественно продолжается.
Тесты данных. Сейчас корректность мы проверяли запросами вручную. В production нужны автоматические проверки качества: ключи не NULL, метрики в разумных диапазонах, после MERGE нет дублей. Их прогоняют на каждый запуск, и пайплайн падает при нарушении.
Оркестрация. Капстоун-пайплайн запускается руками. Production-вариант запускается сам — по расписанию или по событию появления нового файла. DuckDB вписывается в dbt-duckdb и в обычные шедулеры; для облачного варианта расписание даёт MotherDuck.
Мониторинг. Стоит наблюдать за временем расчёта, объёмом спилла, числом строк витрины от запуска к запуску — резкое отклонение это сигнал проблемы в данных или в нагрузке.
И стоит вспомнить альтернативы хранения каталога: капстоун использовал DuckLake с каталогом в SQLite, потому что проект локальный и однопользовательский. Как только пайплайн становится командным и писателей несколько, каталог переносится на PostgreSQL — код пайплайна при этом не меняется, меняется только строка ATTACH.
Что вы построили
Сведём итог капстоуна. За семь уроков собран работающий локальный аналитический lakehouse: сырые помесячные Parquet и грязный CSV на объектном хранилище проходят через staging-очистку, моделируются в партиционированную витрину в DuckLake с ACID и снапшотами, обновляются инкрементально и идемпотентно через MERGE без пересчёта истории, считаются out-of-core при датасете больше RAM, и публикуются компактным Parquet для интерактивных запросов из DuckDB-WASM в браузере.
Один движок — DuckDB — прошёл весь путь: от чтения десятков гигабайт сырья до запроса на вкладке браузера. Ни сервера, ни кластера, ни отдельного движка трансформаций. Это и есть то, ради чего курс шёл «до железа»: понимание векторизованного исполнения, storage-формата, сжатия, параллелизма, larger-than-memory механики и DuckLake складывается в умение спроектировать и собрать такой пайплайн осознанно, а не по шаблону.
Попробуй сам
Финальное задание — провести полноценный code review собственного капстоун-проекта.
Задания:
- Пройдите свой пайплайн по чеклисту code review из урока, пункт за пунктом. Для каждого пункта зафиксируйте «выполнено» или «надо исправить».
- Найдите в своём коде хотя бы одно место, где границу слоёв можно случайно нарушить (бизнес-логика в staging,
SELECT *,INSERTвместоMERGE), и либо подтвердите, что её там нет, либо исправьте. - Намеренно сломайте идемпотентность: замените инкрементальный
MERGEнаINSERT, запустите шаг дважды и убедитесь, что появились дубли. ВернитеMERGEи проверьте, что дублей нет. Это закрепит, почему идемпотентность обязательна. - Для каждого из четырёх ключевых архитектурных решений (слои, движок, DuckLake, партиционирование+MERGE) запишите одним предложением, какую альтернативу вы отвергли и почему. Это и есть то, что вы должны уметь объяснить на собеседовании.
dbt: автоматические тесты данных как production-стандарт