Iceberg в dbt и warehouse-specific tuning
В предыдущих уроках разобрали parsing, threads, selectors, microbatch — это уровень dbt. Сейчас спускаемся на уровень warehouse: Iceberg как открытый табличный формат, Snowflake/BigQuery specific tuning для dbt-jobs.
Это важно для двух причин:
- Iceberg становится de facto стандартом для cross-warehouse data (2025-2026 trend).
- Правильный warehouse setup может сократить cost dbt-jobs в 5-10x.
Iceberg в контексте dbt
Apache Iceberg — открытый table format, который позволяет хранить данные в object storage (S3, GCS, Azure Blob) с транзакционными гарантиями и schema evolution. В 2025 году все major warehouses (Snowflake, BigQuery, Databricks) добавили нативную поддержку Iceberg.
Зачем это нужно для dbt:
- Cross-warehouse читаемость — таблица на Iceberg может читаться из Snowflake, Trino, Spark, DuckDB одновременно.
- No vendor lock-in — данные в S3, не в проприетарном Snowflake format.
- Cost — object storage в 10x дешевле warehouse storage.
- Time travel — Iceberg хранит snapshots, можно SELECT FROM table AS OF ‘2026-05-01’.
dbt поддерживает Iceberg через catalogs.yml (новый файл, появился в dbt-core 1.9).
catalogs.yml — структура
catalogs.yml лежит в корне dbt-проекта (рядом с dbt_project.yml). Определяет, какие external catalogs доступны:
# catalogs.yml
catalogs:
- name: aws_glue
active_write_integration: dev_iceberg
write_integrations:
- name: dev_iceberg
catalog_type: glue
external_volume: my_iceberg_storage
adapter_properties:
file_format: parquet
table_type: iceberg
Поля:
name— логическое имя catalog’а, на которое модели ссылаются.catalog_type— тип catalog (glue для AWS Glue, polaris для Snowflake Polaris, unity для Databricks Unity).external_volume— Snowflake-объект для доступа к S3 (Snowflake-specific).adapter_properties— adapter-specific параметры.
В моделях вы указываете catalog в config():
-- models/iceberg_events.sql
{{ config(
materialized='table',
catalog='aws_glue'
) }}
select
event_id,
event_timestamp,
user_id
from {{ source('app', 'events') }}
dbt при run:
- Создаёт таблицу через Snowflake (или Iceberg-compatible warehouse).
- Файлы parquet кладутся в S3 (через external_volume).
- Metadata записывается в AWS Glue catalog.
- Таблица доступна через любой Iceberg-reader.
Partition pruning на Iceberg
Iceberg хранит partition metadata в catalog. При SELECT с фильтром по partition column — warehouse читает только нужные partition-файлы:
-- Запрос с filter
SELECT * FROM iceberg_events WHERE event_date = '2026-05-15'
Iceberg catalog говорит: “файлы за эту дату — vot эти 100 parquet’ов в S3”. Warehouse читает только их, не сканит весь dataset. Это partition pruning.
Сравнение с обычной таблицей в Snowflake:
| Подход | Скан 1B-row table | Cost (X-Large WH) |
|---|---|---|
| Snowflake native + cluster keys | 5 sec | $0.01 |
| Iceberg + partition pruning | 4 sec | $0.005 |
| Iceberg без partitioning | 60 sec | $0.20 |
| External Parquet без partition metadata | 120 sec | $0.40 |
Iceberg даёт почти нативную производительность Snowflake, но в open format.
Конфигурация partition pruning в dbt
{{ config(
materialized='incremental',
catalog='aws_glue',
partition_by={
'field': 'event_timestamp',
'data_type': 'timestamp',
'granularity': 'day'
},
cluster_by=['user_id']
) }}
dbt при создании таблицы передаёт partition spec в Iceberg catalog. После этого все downstream queries автоматически используют partition pruning.
Iceberg gotcha: после создания таблицы partition spec immutable (в Iceberg v1 spec). Если вы потом меняете partition_by в dbt-конфиге и делаете --full-refresh — таблица переезжает на новый spec. Без full-refresh — старая partition scheme.
Iceberg vs native Snowflake/BigQuery: trade-offs
Iceberg даёт portability и cost saving на storage. Native — лучшую performance и tooling integration.
Реальная архитектура 2026:
- Staging — native warehouse tables (быстрые fact-агрегации).
- Intermediate/marts — native warehouse.
- Exposable data (для других team / Spark ML) — Iceberg.
Через catalog: 'aws_glue' в конкретной модели — она становится Iceberg, остальные native. dbt поддерживает heterogeneous setup.
Snowflake-specific tuning для dbt jobs
Это самый частый stack в production. Конкретные рекомендации:
Warehouse size по типу job
# profiles.yml
production:
outputs:
prod:
type: snowflake
account: my-account
warehouse: DBT_DEFAULT_WH # для большинства моделей
role: TRANSFORMER
threads: 16
В моделях override через config():
-- Маленькие staging-модели
{{ config(snowflake_warehouse='DBT_STAGING_WH') }} -- Small
-- Тяжёлые marts с aggregations
{{ config(snowflake_warehouse='DBT_MARTS_WH') }} -- Large
-- Heavy ML feature engineering
{{ config(snowflake_warehouse='DBT_HEAVY_WH') }} -- X-Large
Auto-suspend для cost
Snowflake warehouse биллится по времени running. Auto-suspend останавливает warehouse через N секунд после последнего query:
ALTER WAREHOUSE DBT_PROD_WH SET AUTO_SUSPEND = 60; -- 1 минута
Default — 600 seconds (10 минут). После одного dbt-job warehouse продолжает крутиться 10 минут впустую. Setting на 60s экономит 90% idle cost.
Trade-off: при холодном старте warehouse есть 1-3 sec overhead. На каждом dbt run это +1-3s, не критично для часовых run-ов.
Multi-cluster warehouses
Для CI и concurrent runs:
CREATE WAREHOUSE DBT_CI_WH WITH
WAREHOUSE_SIZE = 'MEDIUM'
MIN_CLUSTER_COUNT = 1
MAX_CLUSTER_COUNT = 4
AUTO_SCALING_POLICY = 'STANDARD'
AUTO_SUSPEND = 60;
Snowflake автоматически добавляет clusters при concurrent load. На CI с 10 одновременными PR-ами — кластеры масштабируются, каждый PR не ждёт.
Query tagging для observability
-- В dbt_project.yml
on-run-start: "ALTER SESSION SET QUERY_TAG = 'dbt_run|{{ invocation_id }}|{{ target.name }}'"
Теперь все queries dbt-runs тэгированы. В Snowflake Query History можно фильтровать:
SELECT
QUERY_TEXT,
EXECUTION_TIME,
BYTES_SCANNED,
CREDITS_USED_CLOUD_SERVICES
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE QUERY_TAG LIKE 'dbt_run%'
ORDER BY START_TIME DESC;
Это даёт точную attribution cost per dbt run — какие модели сколько потратили.
Resource monitors
CREATE RESOURCE MONITOR DBT_PROD_MONITOR WITH
CREDIT_QUOTA = 1000
FREQUENCY = MONTHLY
NOTIFY_USERS = ('data_engineer1', 'data_engineer2')
TRIGGERS
ON 75 PERCENT DO NOTIFY
ON 90 PERCENT DO SUSPEND
ON 100 PERCENT DO SUSPEND_IMMEDIATE;
Это страховка от cost explosion. Если dbt-job уходит в бесконечный loop из-за бага, warehouse автоматически suspended на 90% месячного бюджета.
BigQuery-specific tuning
BigQuery — другая модель: slots, не warehouses.
On-demand vs Flat-rate (reservations)
On-demand: pay-per-query (TB scanned).
- $5 per TB scanned (US prices).
- Concurrency limit 100 queries/project.
- Хорошо для unpredictable workloads.
Flat-rate (reservations): pay-per-slot-hour.
- Бронируете N slots, queries делят их.
- Concurrency unlimited (но queries делят slots).
- Хорошо для predictable workloads (dbt scheduled runs).
Для dbt в production обычно используется reservations + commit period 1 month:
# profiles.yml
production:
outputs:
prod:
type: bigquery
project: my-gcp-project
dataset: analytics_prod
threads: 24
priority: batch # batch priority cheaper than interactive
priority: batch — это важный параметр. Batch queries имеют lower priority (могут wait), но дешевле в reservation model. Для scheduled dbt-runs batch — natural choice.
Partition pruning
BigQuery аналогично Snowflake требует partition columns. В dbt:
{{ config(
materialized='incremental',
partition_by={
'field': 'event_date',
'data_type': 'date',
'granularity': 'day'
},
cluster_by=['user_id', 'event_type']
) }}
Cluster keys в BigQuery — это secondary sort. Когда query filter включает cluster_by columns, BigQuery читает меньше partitions.
—use_legacy_sql и dialect
Современный BigQuery использует Standard SQL (default). Legacy SQL deprecated. dbt-bigquery всегда использует Standard. Просто избегайте dependencies на legacy syntax в макросах.
Table expiration
Для temp tables / staging:
{{ config(
materialized='table',
expiration_timestamp='TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)'
) }}
BigQuery автоматически удалит таблицу через 30 дней. Полезно для PR-temp schemas — после merge PR schema удаляется без manual cleanup.
DuckDB tuning
В контексте курса — DuckDB как обучающий warehouse. Production tuning тоже актуален для local dev / CI:
my_project:
target: dev
outputs:
dev:
type: duckdb
path: './my_project.duckdb'
threads: 4
settings:
memory_limit: '8GB' # default — 80% RAM, обычно слишком много
temp_directory: '/tmp/duckdb_spill' # disk для spillover на больших aggs
max_memory: '8GB'
memory_limit критичен — без него DuckDB ест ВСЁ memory, что замедляет систему (swap, OOM). Set explicit limit.
temp_directory — для spilling на больших aggregations. Без него — OOM. С — disk-based fallback (медленнее но не падает).
DuckDB extensions
extensions:
- httpfs # S3/HTTP reads
- parquet # Parquet files
- json # JSON parsing
Extensions — это native libraries, которые расширяют DuckDB. Для dbt с S3-sources нужны httpfs + parquet.
Comprehensive prod setup: пример
Объединённый пример для production-проекта на Snowflake с Iceberg:
# catalogs.yml
catalogs:
- name: aws_glue_iceberg
active_write_integration: prod
write_integrations:
- name: prod
catalog_type: glue
external_volume: s3_external_vol
adapter_properties:
file_format: parquet
table_type: iceberg
# profiles.yml
production:
target: prod
outputs:
prod:
type: snowflake
account: my-account
user: dbt_runner_svc
role: TRANSFORMER
database: ANALYTICS
schema: PROD
warehouse: DBT_DEFAULT_WH
threads: 16
private_key_path: '/secrets/dbt_runner.p8'
-- dbt_project.yml
models:
my_project:
+on-run-start: "ALTER SESSION SET QUERY_TAG = 'dbt|{{ invocation_id }}'"
staging:
+materialized: view
+snowflake_warehouse: DBT_STAGING_WH
intermediate:
+materialized: table
+snowflake_warehouse: DBT_MARTS_WH
marts:
+materialized: table
+snowflake_warehouse: DBT_MARTS_WH
iceberg_exports:
+catalog: aws_glue_iceberg
+materialized: incremental
+partition_by:
field: event_date
data_type: date
granularity: day
С этим setup:
- Staging — small warehouse, fast view rebuild.
- Intermediate/marts — large warehouse, table materialization.
- Iceberg exports — open format для downstream ML/sharing.
- Query tagging — full cost attribution в Snowflake usage.
CI/CD tuning для cost
Для PR-ов на CI добавьте:
ci:
outputs:
ci:
type: snowflake
warehouse: DBT_CI_WH # multi-cluster small
schema: 'pr_{{ env_var("PR_NUMBER") }}'
threads: 24
Schema per PR — это паттерн dynamic schemas. После merge PR schema удаляется (через scheduled cleanup job).
-- Cleanup script запускается раз в день
DROP SCHEMA IF EXISTS ANALYTICS.PR_123 CASCADE;
Это даёт isolated CI runs без data conflicts между PR-ами.
Real-world cost saving: команда из 20 разработчиков на Snowflake Medium warehouse без CI tuning тратит ~2-3K/мес. Это 80% saving на тех же запросах.
Performance benchmark на realistic project
Конкретные numbers с production-проекта на Snowflake + Iceberg:
| Operation | Native Snowflake | Iceberg | Difference |
|---|---|---|---|
| INSERT 100M rows | 45s | 90s | Iceberg 2x slower (file format overhead) |
| SELECT с partition filter | 5s | 6s | Almost same |
| Full scan 1B rows | 60s | 80s | Iceberg slower (S3 latency) |
| dbt —full-refresh 500GB table | 25 min | 35 min | Iceberg 40% slower |
| Storage cost (1TB/year) | $25/mo (Snowflake) | $3/mo (S3) | Iceberg 8x cheaper |
Iceberg slower на writes, но storage cost compensates. Для mostly-read datasets — Iceberg рекомендуется.
Резюме
- Iceberg — open table format в S3/GCS. Cross-warehouse readable, cheap storage.
- catalogs.yml — dbt 1.9+ конфиг для Iceberg/external catalogs.
- Partition pruning на Iceberg работает аналогично native warehouse, но immutable spec после create.
- Snowflake tuning: разные warehouses per layer, auto-suspend 60s, query tagging, resource monitors.
- BigQuery tuning: reservations + batch priority, partition_by + cluster_by, table expiration для temp.
- DuckDB tuning: memory_limit, temp_directory обязательны для production-like CI.
- Hybrid architecture — Iceberg только для cross-system marts, native везде остальное.
- Cost engineering: Slim CI + per-PR schemas + multi-cluster — экономит 80%+ от naive setup.