Learning Platform
Глоссарий Troubleshooting
Урок 07.02 · 17 мин
Начальный
tableCREATE TABLEperformancefull refresh

В прошлом уроке мы поняли, что view — это сохранённый запрос без данных. Иногда этого хватает, иногда — нет. Table материализация даёт другой trade-off: dbt создаёт настоящую таблицу с данными. Запрос на материализацию выполняется один раз (при dbt run), а downstream-чтения становятся почти мгновенными.

Это второй после view самый используемый тип материализации. Понимать его — must для junior’а.

Что делает dbt при materialized=‘table’

dbt компилирует SQL модели в полный путь, оборачивает его в DDL:

CREATE OR REPLACE TABLE jaffle_shop.main.dim_customers AS (
    SELECT ...
);

Warehouse выполняет: считает запрос, материализует результат в таблицу, заменяет старую. На DuckDB операция атомарная (либо успешно — старая полностью заменена, либо нет — старая жива).

dbt run на table-материализации

dbt отправляет CREATE OR REPLACE TABLE в warehouse. Warehouse считает запрос, материализует результат в физические данные. Старая версия таблицы заменяется атомарно.

models/marts/dim_customers.sqlconfig(materialized='table')
dbt runкомпилирует Jinja и обёртку
Финальный SQLCREATE OR REPLACE TABLE ... AS SELECT ...
Warehouseвычисляет, материализует, заменяет
dim_customers в warehouseфизические данные, можно SELECT мгновенно

После создания таблицы любой downstream-SELECT — это просто чтение, без пересчёта SQL модели. Latency минимальна, потому что warehouse не повторяет JOIN/GROUP BY каждый раз.

Когда переходить от view к table

Самый частый триггер: запрос дорогой, downstream-обращений много. Несколько конкретных индикаторов:

1. Время выполнения SELECT > 5 секунд. Если view выполняется 5+ секунд, и на неё ссылаются больше 2 моделей — переводи в table. Пользователи замечают latency, аналитики бесятся от тормозящего BI.

2. Запрос содержит aggregation на больших таблицах. GROUP BY с millions of rows, оконные функции, многоступенчатые JOIN — кандидаты на table. Расчёт один раз, downstream — мгновенно.

3. Модель — mart, который читается дашбордами. Marts по конвенции — всегда table. Они потребляются множеством пользователей, latency критична. Свежесть достигается через регулярный dbt run.

4. Деревьвозможно глубокий dependency tree. Если ниже по DAG ещё 3 уровня моделей, материализовать промежуточный слой как table — это разорвать цепочку view->view->view->…, тем самым уменьшить compile time для warehouse.

View vs table: когда что лучше

View — для дешёвых staging-преобразований с редким downstream. Table — для дорогих aggregations с активным downstream-traffic'ом. Промежуточная зона — оценивай по конкретике.

View хорошоstaging, менее 100MB, дешёвый SELECT, 1-3 downstream
viewПреимущества view: данные всегда свежие, не занимает storage, dbt run быстрый. Подходит для слоя staging, где почти ничего не делается кроме переименований/cast.
Table хорошоmarts, > 1M rows, тяжёлый JOIN/GROUP BY, много downstream
tableПреимущества table: SELECT мгновенный, downstream не повторяет JOIN. Цена: storage и медленный dbt run. Подходит для marts и тяжёлых intermediate-моделей.
Серая зонаintermediate, средняя сложность, 2-5 downstream
dependsОценивай: время выполнения, частоту downstream, размер. Можно начать с view и переключить в table, когда дашборды начнут тормозить.

Минусы table-материализации

1. Cтоит storage. На DuckDB — место на диске. На Snowflake — это деньги ($/TB/month). Для junior проекта обычно неважно, но в проде на больших данных учитывается.

2. Данные могут устареть. После dbt run таблица “заморожена”. Если raw обновился через час, table-mart всё ещё показывает старые данные. Решение: запускать dbt run по расписанию или включать в pipeline после loader-а.

3. Дорогой пересчёт при каждом run. Если таблица 10GB, и dbt run делается каждые 15 минут — это 40 пересчётов в час на 10GB данных. Здесь нужен incremental — он считает только дельту.

4. Атомарность не всегда полная. CREATE OR REPLACE TABLE на некоторых warehouse — это DROP + CREATE под капотом. Между этими операциями короткое окно, когда таблица пуста. На DuckDB это не проблема (single writer per file, атомарно), но на cloud warehouse’ах — иногда заметная.

WARNING

На больших таблицах (10GB+) полный пересчёт через CREATE OR REPLACE TABLE может занимать минуты-часы и blocking-ить downstream. Это сигнал, что пора переходить на incremental: считать только новые/изменённые строки. Подробнее в следующем модуле.

Анатомия скомпилированного SQL для table

Возьмём типичный mart:

{{ config(materialized='table') }}

SELECT
    c.customer_id,
    c.email,
    COUNT(o.order_id) AS orders_count,
    SUM(o.amount)     AS total_spent
FROM {{ ref('stg_customers') }} c
LEFT JOIN {{ ref('stg_orders') }} o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.email

После компиляции (в target/compiled/...):

SELECT
    c.customer_id,
    c.email,
    COUNT(o.order_id) AS orders_count,
    SUM(o.amount)     AS total_spent
FROM jaffle_shop.main.stg_customers c
LEFT JOIN jaffle_shop.main.stg_orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.email

В target/run/... будет с обёрткой:

CREATE OR REPLACE TABLE jaffle_shop.main.dim_customers AS (
    SELECT
        c.customer_id,
        c.email,
        COUNT(o.order_id) AS orders_count,
        SUM(o.amount)     AS total_spent
    FROM jaffle_shop.main.stg_customers c
    LEFT JOIN jaffle_shop.main.stg_orders o ON c.customer_id = o.customer_id
    GROUP BY c.customer_id, c.email
);

Именно этот SQL отправляется в DuckDB и выполняется при dbt run.

Schema и database для table

По умолчанию таблица создаётся в target.schema из profiles.yml. Можно переопределить через config:

{{ config(
    materialized='table',
    schema='marts',
    database='analytics'
) }}

SELECT ...

С учётом схема-комбинирования (схема в результате = target.schema + '_' + custom_schema), это создаст таблицу в analytics.<target.schema>_marts.dim_customers. На DuckDB смена database через config — это попытка использовать другой файл, обычно не нужно. На Snowflake/BigQuery — нормальный паттерн.

DuckDB-специфика для table

В DuckDB table — это самый распространённый сценарий, потому что:

  1. CREATE OR REPLACE TABLE атомарно. Single writer per file гарантирует, что downstream чтения видят либо старую, либо новую версию, но не промежуточное состояние.
  2. Storage в файле .duckdb. Каждая таблица занимает блоки в едином файле. Нет пер-таблиц storage cost — есть просто размер файла.
  3. TPC-DS performance отличный. DuckDB — vectorized engine, простые table-mart-запросы летают на single-machine миллиардами строк.
  4. PRAGMA threads контролирует параллелизм. Если CREATE TABLE долгий, можно через profiles.yml настроить threads: 8.
  5. No PARTITION BY. В отличие от BigQuery, DuckDB не имеет нативного partitioning. Если нужны big-data optimization — рассмотри external materialization (Parquet с partition-prefix).
NOTE

В реальной работе на Snowflake/BigQuery table-материализация может использовать CLUSTER BY, PARTITION BY и другие фичи, которые DuckDB не поддерживает. На junior-проекте на DuckDB этим можно не заморачиваться — модели до десятков миллионов строк прекрасно живут без оптимизаций.

Команды для отладки table

# Запустить только одну модель как table
dbt run --select dim_customers

Output:

14:23:01  Concurrency: 4 threads
14:23:01  1 of 1 START sql table model main.dim_customers ......... [RUN]
14:23:02  1 of 1 OK created sql table model main.dim_customers .... [OK in 0.81s]

Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1

“sql table model” подтверждает материализацию. Время больше, чем у view, потому что нужно реально пересчитать.

Проверить в DuckDB:

SELECT table_name, table_type FROM information_schema.tables WHERE table_name = 'dim_customers';

Получишь BASE TABLE в table_type — это и есть полноценная таблица с данными.

Посмотреть размер:

SELECT COUNT(*) FROM dim_customers;

Полный пересчёт vs incremental: куда дальше

Table-материализация при каждом dbt run полностью пересоздаёт таблицу. Если в исходных данных миллион новых строк, а в твоей таблице уже было 100M строк — все 100M+1M пересчитываются заново.

Это не проблема для маленьких таблиц (менее 10M строк на DuckDB) или для models, где запрос быстрый. Но на больших фактах с историей за годы — это становится медленно.

Решение — incremental materialization, которая считает только дельту. Это следующий шаг сложности, и ему посвящён модуль 7. Пока просто запомни: table = полный пересчёт, incremental = дельта. Выбирай table, пока tot оs пересчёт укладывается в приемлемое время.

Попробуй сам

В своём проекте создай models/marts/dim_customers_demo.sql:

{{ config(materialized='table') }}

SELECT
    customer_id,
    first_name || ' ' || last_name AS full_name
FROM {{ ref('stg_customers') }}

Запусти:

dbt run --select dim_customers_demo

Проверь:

duckdb jaffle_shop.duckdb -c "SELECT table_name, table_type FROM information_schema.tables WHERE table_name='dim_customers_demo';"

Должен быть BASE TABLE. Запусти dbt run ещё раз — увидишь то же время, потому что dbt пересоздаёт таблицу с нуля. Это и есть “полный пересчёт”.

Затем поменяй на materialized='view'. Запусти. Проверь — будет VIEW. Сравни время выполнения dbt run между table и view: view быстрее, потому что только метаданные.

Что мы поняли

Table — материализация, при которой dbt создаёт физическую таблицу через CREATE OR REPLACE TABLE ... AS SELECT. Downstream-чтения мгновенные, но dbt run пересчитывает всё с нуля при каждом запуске. Используется для marts и тяжёлых intermediate-моделей. На DuckDB атомарна и быстра, на cloud warehouses — нужно думать о cost. Когда полный пересчёт становится медленным — следующий шаг это incremental, разберём в модуле 7.

В следующем уроке — ephemeral, материализация-CTE, которая не создаёт ничего в warehouse, но раскрывается inline в downstream-запросы.

Как DuckDB хранит таблицу физически — row groups и columnar storage
Проверка знанийKnowledge check
Ты материализовал dim_customers как table, и через месяц использования заметил, что dbt run на этой модели занимает 12 минут — потому что в orders уже 50M строк. Что делать?
ОтветAnswer
Несколько опций: (1) если запрос можно ограничить только новыми данными по дате — перевести в incremental materialization, считать только новые строки за период; (2) добавить фильтр на recent данных в SQL модели, если history не критична; (3) если запрос неэффективен — переписать SQL: проверить план выполнения, убрать ненужные JOIN, использовать материализованные intermediate-слои. На DuckDB можно увеличить threads в profiles.yml для параллелизма, но это лишь линейное ускорение. Главный путь к real-time performance — incremental.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Какой DDL/DML dbt отправляет в warehouse при materialized='table'?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4