Learning Platform
Глоссарий Troubleshooting
Урок 07.03 · 25 мин
Средний
audit_helpercodegenMigrationAuto-generationPackages

audit_helper и codegen: миграции и автогенерация

Два уникальных package в dbt экосистеме: dbt_audit_helper (для миграций моделей) и dbt_codegen (для auto-generation YAML и SQL). Они не нужны постоянно — но в нужный момент economically спасают часы работы.

Этот урок — когда какой package нужен, и production workflow patterns.

SCD Type 2: когда миграция модели меняет историческую логику

dbt_audit_helper: сравнение old vs new

Use case: рефакторите модель fct_orders. Новая реализация должна давать те же результаты, что старая (no business logic changes). Как это проверить?

Без audit_helper — пишите вручную:

-- diff query вручную
WITH old AS (
    SELECT * FROM analytics.fct_orders_old
),
new AS (
    SELECT * FROM analytics.fct_orders_new
)
SELECT 'in_old_not_new' AS where, * FROM old EXCEPT SELECT * FROM new
UNION ALL
SELECT 'in_new_not_old' AS where, * FROM new EXCEPT SELECT * FROM old

audit_helper делает это в одну строку.

Setup

# packages.yml
packages:
  - package: dbt-labs/audit_helper
    version: [">=0.10.0", "<1.0.0"]
dbt deps

Базовое сравнение: compare_relations

-- analyses/audit_fct_orders_migration.sql
{{ audit_helper.compare_relations(
    a_relation=ref('fct_orders_old'),
    b_relation=ref('fct_orders_new'),
    primary_key='order_id',
    exclude_columns=['_loaded_at']
) }}

Compile output:

-- Compare summary
WITH a AS (SELECT * FROM analytics.fct_orders_old),
b AS (SELECT * FROM analytics.fct_orders_new),
diff AS (
    SELECT 'in_a_not_b' AS source, * FROM a EXCEPT SELECT 'in_a_not_b', * FROM b
    UNION ALL
    SELECT 'in_b_not_a' AS source, * FROM b EXCEPT SELECT 'in_b_not_a', * FROM a
)
SELECT source, COUNT(*) AS n_records FROM diff GROUP BY source

Run:

dbt compile --select audit_fct_orders_migration
dbt show --select audit_fct_orders_migration

Result:

source       | n_records
in_a_not_b   | 0
in_b_not_a   | 0

Если обе строки — 0, миграция bit-exact. Если non-zero — есть differences, investigate.

Per-row comparison: compare_queries

Более детальный compare — row-by-row:

-- analyses/audit_fct_orders_per_row.sql
{{ audit_helper.compare_queries(
    a_query='SELECT * FROM ' ~ ref('fct_orders_old'),
    b_query='SELECT * FROM ' ~ ref('fct_orders_new'),
    primary_key='order_id',
    exclude_columns=['_loaded_at']
) }}

Возвращает rows со differences и которые колонки отличаются.

Column-level diff: compare_column_values

-- analyses/audit_amount_only.sql
{{ audit_helper.compare_column_values(
    a_query='SELECT order_id, amount FROM ' ~ ref('fct_orders_old'),
    b_query='SELECT order_id, amount FROM ' ~ ref('fct_orders_new'),
    primary_key='order_id',
    column_to_compare='amount'
) }}

Возвращает summary:

  • Match (одинаковое значение).
  • a_only / b_only (есть только в одной).
  • Different (в обеих, но разные значения).

Полезно для targeted comparison: ‘мы изменили calculation amount, есть ли differences?’.


Production migration workflow

Реальная миграция модели через audit_helper:

Step 1: build both versions

-- models/marts/fct_orders.sql — OLD (current production)
SELECT ...

-- models/marts/fct_orders_v2.sql — NEW (refactored)
SELECT ...  -- new implementation, target same logic
dbt build --select fct_orders fct_orders_v2

Обе модели materialized в warehouse.

Step 2: high-level diff

-- analyses/audit_fct_orders_migration.sql
{{ audit_helper.compare_relations(
    a_relation=ref('fct_orders'),
    b_relation=ref('fct_orders_v2'),
    primary_key='order_id',
    exclude_columns=['_loaded_at', '_etl_run_id']
) }}
dbt compile --select audit_fct_orders_migration
dbt show

Если differences — get count by source.

Step 3: investigate differences

Если in_a_not_b > 0 — есть rows в OLD не в NEW (NEW потерял rows).

-- analyses/audit_missing_in_v2.sql
SELECT order_id FROM {{ ref('fct_orders') }}
EXCEPT
SELECT order_id FROM {{ ref('fct_orders_v2') }}
LIMIT 100

Investigate какие order_id missing. Common causes:

  • WHERE filter changed.
  • JOIN type changed (INNER vs LEFT).
  • Source filter slipped.

Step 4: per-column comparison

Если row counts match, но content differs:

{{ audit_helper.compare_column_values(
    a_query='SELECT order_id, amount FROM ' ~ ref('fct_orders'),
    b_query='SELECT order_id, amount FROM ' ~ ref('fct_orders_v2'),
    primary_key='order_id',
    column_to_compare='amount'
) }}

Repeat для каждой важной колонки. Identify which calculations diverged.

Step 5: cutover

После 100% match (или acceptable known diff):

  1. Renaming: fct_orders.sql -> archive, fct_orders_v2.sql -> fct_orders.sql.
  2. Update downstream models if needed.
  3. PR review.
  4. Deploy.

audit_helper диагностические analyses можно оставить в репо как proof of equivalence.


Common audit_helper patterns

Tolerance для numeric

Numeric differences могут быть float artifacts. audit_helper не имеет native tolerance, но можно wrap:

{{ audit_helper.compare_queries(
    a_query=\"SELECT order_id, ROUND(amount, 2) AS amount FROM \" ~ ref('fct_orders_old'),
    b_query=\"SELECT order_id, ROUND(amount, 2) AS amount FROM \" ~ ref('fct_orders_new'),
    primary_key='order_id'
) }}

ROUND до 2 decimals = ignores float noise.

Sample testing

На больших tables — diff на subset:

{{ audit_helper.compare_queries(
    a_query=\"SELECT * FROM \" ~ ref('fct_orders_old') ~ \" WHERE order_date >= current_date - 7\",
    b_query=\"SELECT * FROM \" ~ ref('fct_orders_new') ~ \" WHERE order_date >= current_date - 7\",
    primary_key='order_id'
) }}

Last week only — fast diff. Если совпадает, expand window.

Per-environment diff

-- Сравнить dev vs prod
{{ audit_helper.compare_queries(
    a_query=\"SELECT * FROM dev_analytics.fct_orders\",
    b_query=\"SELECT * FROM prod_analytics.fct_orders\",
    primary_key='order_id'
) }}

Catches drift между environments. Useful если dev должен mirror prod.


dbt_codegen: auto-generation

dbt_codegen генерирует YAML / SQL automatically на based on warehouse schema. Use cases:

  1. Bootstrap source YAML — для new source database с 100 tables.
  2. Bootstrap staging models — typing SELECT col1, col2, ... для каждой source table.
  3. Generate base model YAML — column descriptions, типы.

Setup

# packages.yml
packages:
  - package: dbt-labs/codegen
    version: [">=0.13.0", "<1.0.0"]
dbt deps

generate_source: создание YAML для source

dbt run-operation generate_source --args '{schema_name: app, database_name: raw}'

Output (печатает в stdout):

version: 2

sources:
  - name: app
    database: raw
    tables:
      - name: customers
      - name: orders
      - name: order_items
      - name: products
      - name: payments

Save into models/sources/_sources.yml. Manual review, добавление descriptions, freshness, tests.

С extras:

dbt run-operation generate_source --args '{
    schema_name: app,
    database_name: raw,
    generate_columns: true,
    include_descriptions: true
}'

Generates full YAML с columns:

sources:
  - name: app
    database: raw
    tables:
      - name: customers
        description: ""  # to be filled
        columns:
          - name: customer_id
            description: ""
          - name: name
            description: ""
          - name: email
            description: ""

Затем добавляете descriptions manually.

generate_base_model: bootstrap staging

dbt run-operation generate_base_model --args '{
    source_name: app,
    table_name: customers
}'

Output:

WITH source AS (
    SELECT * FROM {{ source('app', 'customers') }}
),

renamed AS (
    SELECT
        customer_id,
        first_name,
        last_name,
        email,
        created_at,
        updated_at
    FROM source
)

SELECT * FROM renamed

Save в models/staging/stg_app__customers.sql. Modify rename column logic.

С extras:

dbt run-operation generate_base_model --args '{
    source_name: app,
    table_name: customers,
    leading_commas: true
}'

Стиль leading commas (some teams prefer).

generate_model_yaml: создание YAML

После создания model:

dbt run-operation generate_model_yaml --args '{
    model_names: [stg_app__customers, stg_app__orders]
}'

Output:

version: 2
models:
  - name: stg_app__customers
    description: ""
    columns:
      - name: customer_id
        description: ""
      - name: first_name
        description: ""
      - name: email
        description: ""

Save в models/staging/_models.yml. Manual add: descriptions, tests.

Production workflow

New project bootstrap:

  1. dbt run-operation generate_source --args '{schema_name: app}' -> save into _sources.yml.
  2. For each table:
dbt run-operation generate_base_model --args '{source_name: app, table_name: customers}' > models/staging/stg_app__customers.sql
  1. dbt run-operation generate_model_yaml --args '{model_names: [...]}' -> save into _models.yml.
  2. Manual: add tests, descriptions, transformations.

Saves hours для project с 50+ tables.

New table addition:

  1. Source-team added new table app.payments.
  2. dbt run-operation generate_base_model --args '{source_name: app, table_name: payments}' -> save.
  3. Add source entry in _sources.yml.
  4. Add model YAML.

Without codegen — manual typing all column names from DESCRIBE TABLE.


Gotchas

audit_helper

  1. Performance: для huge tables — compare очень тяжёлый. EXCEPT scans both relations. Use sample / where фильтры.

  2. Float precision: numeric differences часто float noise. Round before compare.

  3. Order independence: audit_helper does not care row order. Если оба относительно одинаковые но в разном порядке — ok.

  4. NULL handling: NULL != NULL в SQL. Rows с NULL могут appear в diff даже если semantically одинаковы. Use COALESCE в queries.

  5. Schema differences: разные column orders, разные types — compare может fail на implicit cast issues. Align schemas via SELECT explicit columns.

codegen

  1. Need warehouse access: codegen reads schema через adapter.get_columns_in_relation. Warehouse должна быть accessible.

  2. No business logic: generates structural skeleton. Все business renaming, transformations, tests — manual.

  3. Re-generation overwrites: если manually добавили columns, re-run codegen затирает changes. Save outputs cautiously.

  4. Output to stdout: codegen prints, не пишет в файлы. Use shell redirection:

dbt run-operation generate_base_model --args '...' > models/staging/stg_xxx.sql
  1. Pre-1.5 syntax: некоторые codegen versions use older YAML syntax. Проверяйте output на compliance с current dbt version.

Combining patterns

Pattern: large source onboarding.

# Step 1: generate source YAML
dbt run-operation generate_source --args '{schema_name: app, generate_columns: true}' > models/sources/_sources.yml

# Step 2: for each table, generate base model
for table in customers orders order_items products payments; do
  dbt run-operation generate_base_model --args "{source_name: app, table_name: $table}" > models/staging/stg_app__$table.sql
done

# Step 3: generate model YAML
dbt run-operation generate_model_yaml --args '{model_names: [stg_app__customers, stg_app__orders, ...]}' > models/staging/_models.yml

# Step 4: manual review, edit, add tests

5 tables × 5 minutes = 25 minutes vs 5 hours of manual typing.

Pattern: model refactor verification.

# 1. Build both versions
dbt build --select fct_orders fct_orders_v2

# 2. Compare
dbt compile --select audit_fct_orders_migration
dbt show

# 3. If diff:
dbt compile --select audit_missing_in_v2
dbt show

# 4. Iterate v2 until 100% match
# 5. Rename, commit, deploy

DuckDB specifics

audit_helper и codegen работают на DuckDB. Особенности:

  • codegen read schema через DuckDB system tables. Тип BIGINT, TIMESTAMP — DuckDB-specific defaults.
  • audit_helper EXCEPT — DuckDB поддерживает EXCEPT BY NAME (column matching by name, not position) с extensions. Standard EXCEPT работает.

Performance на DuckDB обычно good для these operations (in-memory processing).


Попробуй сам

В labs:

  1. audit_helper migration workflow:

    • Create fct_orders_v2 — refactored version of fct_orders.
    • Use compare_relations для high-level diff.
    • Use compare_column_values для targeted column diff.
    • Get to 100% equivalence.
  2. codegen для new source:

    • generate_source для имеющейся schema.
    • generate_base_model для каждой table.
    • generate_model_yaml для документации.
    • Review output, manually добавьте descriptions.
  3. Combined workflow: используйте codegen для нового source, проверьте через audit_helper что новые staging models возвращают ту же data что source.


Ключевые выводы

  1. audit_helper — для migration verification. compare_relations (high-level), compare_queries (per-row), compare_column_values (per-column).
  2. codegen — для auto-generation YAML / SQL skeletons. generate_source, generate_base_model, generate_model_yaml.
  3. audit_helper workflow: build both versions -> compare relations -> investigate differences -> iterate -> cutover.
  4. codegen workflow: generate skeletons -> manually add business logic / descriptions / tests.
  5. Numeric precision: round before audit_helper compare. NULL handling — COALESCE.
  6. codegen output to stdout — use shell redirection для files. Don’t overwrite manual edits.
  7. DuckDB: оба packages работают, performance в memory good.
Проверка знанийKnowledge check
Команда refactor-ит fct_orders (changed JOIN logic). audit_helper.compare_relations показывает 'in_a_not_b: 100, in_b_not_a: 0'. Что это значит и как investigate?
ОтветAnswer
'in_a_not_b: 100' означает: в **OLD** (a_relation) есть 100 rows, которые **отсутствуют** в NEW (b_relation). NEW потеряла 100 rows при refactor.\n\n'in_b_not_a: 0' означает: NEW не добавил никаких новых rows. Хорошо — мы не invent data.\n\nNet effect: NEW model **на 100 rows меньше** OLD. Investigation:\n\n**Step 1: Identify missing rows.**\n\n```sql\n-- analyses/audit_missing_orders.sql\nWITH old_orders AS (\n SELECT order_id, customer_id, order_date, amount FROM {{ ref('fct_orders') }}\n),\nnew_orders AS (\n SELECT order_id, customer_id, order_date, amount FROM {{ ref('fct_orders_v2') }}\n)\nSELECT * FROM old_orders\nWHERE order_id NOT IN (SELECT order_id FROM new_orders)\nLIMIT 100\n```\n\nRun:\n```bash\ndbt compile --select audit_missing_orders\ndbt show\n```\n\nNow you have 100 specific order_ids that are в OLD но не в NEW.\n\n**Step 2: Look for patterns.**\n\nQuestions to answer от sample 100 missing rows:\n\n1. **All from one date range?**\n```sql\nSELECT MIN(order_date), MAX(order_date) FROM missing_orders;\n-- All from 2020? Maybe filter 'не меньше2021' was added.\n```\n\n2. **All from specific customer / region?**\n```sql\nSELECT customer_country, COUNT(*) FROM missing_orders GROUP BY 1;\n-- All from country = 'XX'? Country filter changed.\n```\n\n3. **All NULL in some column?**\n```sql\nSELECT \n COUNT(*) FILTER (WHERE customer_id IS NULL) AS null_customer,\n COUNT(*) FILTER (WHERE amount = 0) AS zero_amount\nFROM missing_orders;\n-- All have NULL customer_id? INNER JOIN dropped them.\n```\n\n4. **Specific status?**\n```sql\nSELECT status, COUNT(*) FROM missing_orders GROUP BY 1;\n-- All cancelled? Filter changed to exclude cancelled.\n```\n\n**Step 3: Check refactor code для these conditions.**\n\nOpen fct_orders_v2.sql, look for:\n\n- WHERE conditions added (filtering out rows).\n- INNER JOIN replacing LEFT JOIN (drops rows with NULL match).\n- Subquery / CTE with restrictive WHERE.\n\nCommon culprits:\n\n1. **JOIN type change**:\n```sql\n-- OLD\nFROM stg_orders o LEFT JOIN stg_customers c ON o.customer_id = c.id\n\n-- NEW (introduced bug)\nFROM stg_orders o INNER JOIN stg_customers c ON o.customer_id = c.id\n-- Orders with NULL customer_id или customer not in stg_customers — dropped!\n```\n\n2. **Filter введён**:\n```sql\n-- NEW added WHERE\nWHERE status != 'cancelled' -- excludes cancelled orders\n```\n\n3. **Date range filter**:\n```sql\nWHERE order_date не меньше '2024-01-01' -- excluded historical\n```\n\n4. **CTE с DISTINCT**:\n```sql\nWITH unique_customers AS (\n SELECT DISTINCT customer_id FROM ... -- removed duplicates\n)\nSELECT * FROM stg_orders o JOIN unique_customers uc ON o.customer_id = uc.customer_id\n-- если customer был duplicate в OLD source — теперь только one orders preserved\n```\n\n**Step 4: Fix or document.**\n\nTwo paths:\n\n**Path A: It's a bug** — fix the refactor:\n- Change INNER back to LEFT.\n- Remove unintended filter.\n- Re-run dbt build, re-run audit_helper.\n\n**Path B: It's intentional** — document the diff:\n- Add to release notes: 'NEW excludes cancelled orders (was bug in OLD).'\n- Update fct_orders documentation.\n- Communicate to BI / downstream owners.\n\nDecision: depends on business intent. If business approved exclusion — that's a feature, not bug. If not — fix.\n\n**Step 5: Re-verify.**\n\nAfter fix:\n\n```bash\ndbt build --select fct_orders_v2\ndbt compile --select audit_fct_orders_migration\ndbt show\n# Expect: in_a_not_b: 0, in_b_not_a: 0\n```\n\n**Step 6: Per-column comparison.**\n\nEven if row counts match, content might differ (different amount calculations). Check critical columns:\n\n```sql\n{{ audit_helper.compare_column_values(\n a_query='SELECT order_id, amount FROM ' ~ ref('fct_orders'),\n b_query='SELECT order_id, amount FROM ' ~ ref('fct_orders_v2'),\n primary_key='order_id',\n column_to_compare='amount'\n) }}\n```\n\nIf all match — full equivalence achieved. Safe to migrate.\n\n**Step 7: Production cutover.**\n\n1. Final PR review.\n2. Merge.\n3. dbt build на prod.\n4. Monitor downstream BI / models.\n5. Keep audit_helper analyses в repo as reference (proof of equivalence at migration time).\n\n**Key principle**: audit_helper не fix-it tool. It's diagnostic. Investigation + fix manual.
Проверка знанийKnowledge check
codegen generates skeleton stg_app__customers.sql. После manual edit (добавили rename, business logic), нужно re-generate из-за schema change в source. Что делать чтобы не потерять edits?
ОтветAnswer
Classic problem с code generation tools: re-generate overwrites manual edits.\n\nApproaches:\n\n**1. Diff-based merge.**\n\nИспользуйте git как safety net. Workflow:\n\n```bash\n# Save current edited version\ngit add models/staging/stg_app__customers.sql\ngit commit -m \"current state of stg_app__customers\"\n\n# Generate new version (overwrites!)\ndbt run-operation generate_base_model --args '{source_name: app, table_name: customers}' > models/staging/stg_app__customers.sql\n\n# See what changed\ngit diff models/staging/stg_app__customers.sql\n\n# Cherry-pick: manually merge new columns into your edited version\n# Or use 3-way merge tool: keep your edits, accept new columns\n\n# Restore your edits if generation lost them\ngit checkout HEAD -- models/staging/stg_app__customers.sql\n```\n\nIn worst case, restore from commit, manually add new columns by inspecting diff.\n\n**2. Layer pattern (recommended).**\n\nSeparate skeleton from business logic:\n\n```sql\n-- models/staging/base/stg_app__customers_base.sql\n-- (generated by codegen — keep regenerable)\nWITH source AS (\n SELECT * FROM {{ source('app', 'customers') }}\n),\nrenamed AS (\n SELECT\n customer_id,\n first_name,\n last_name,\n email,\n created_at,\n updated_at\n FROM source\n)\nSELECT * FROM renamed\n```\n\n```sql\n-- models/staging/stg_app__customers.sql\n-- (manual — business logic on top of base)\nWITH base AS (\n SELECT * FROM {{ ref('stg_app__customers_base') }}\n),\nenriched AS (\n SELECT\n customer_id,\n TRIM(LOWER(email)) AS email, -- business normalization\n first_name || ' ' || last_name AS full_name, -- composition\n DATE(created_at) AS created_date, -- type conversion\n updated_at,\n CASE WHEN created_at не меньше current_date - 30 THEN 'new' ELSE 'existing' END AS lifecycle_stage\n FROM base\n)\nSELECT * FROM enriched\n```\n\nNow codegen overwrites только _base.sql. Manual logic safe в main file.\n\nWhen source schema changes:\n1. Re-generate _base.sql (overwrite).\n2. If new column added — update main file to include it.\n3. If removed — remove from main file.\n\nClear separation, easy maintenance.\n\n**3. Codegen для skeleton only, manual edit.**\n\nUse codegen **once** для initial skeleton. After that — manual maintenance. Don't re-generate.\n\nWhen source adds new column:\n- Notice it via tests on schema (dbt-expectations expect_table_column_count_to_equal).\n- Manually update stg model.\n\nWorks для small number of changes. Doesn't scale to 50+ tables с frequent source changes.\n\n**4. Custom codegen wrapper.**\n\nWrite custom macro that uses codegen logic + preserves edits:\n\n```sql\n-- macros/regenerate_staging.sql\n{% macro regenerate_staging(source_name, table_name) %}\n {# Steps:\n 1. Read current staging file.\n 2. Extract 'enriched' section (business logic).\n 3. Regenerate 'source' и 'renamed' sections from current schema.\n 4. Concatenate. Write back.\n #}\n ...\n{% endmacro %}\n```\n\nComplex но automated. Investment в tooling.\n\n**Recommendation для large projects**:\n\n1. **Layer pattern** (Approach 2) — cleanest separation.\n - Renamed in _base.sql (regenerable).\n - Business logic in main.sql (manual).\n2. **Tests на schema** (Approach 3) — catches new columns automatically.\n - expect_table_column_count_to_equal.\n - expect_table_columns_to_match_set.\n3. **Git workflow** (Approach 1) — safety net для accidental overwrites.\n\nCombining all three: structural codegen-able layer + business logic on top + tests for drift detection + git для recovery.\n\nWithout layer pattern, codegen ROI diminishes after initial bootstrap. Plan layered architecture upfront.

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

Результат: 0 из 0
Аналитический
Вопрос 1 из 6. Команда refactor-ит fct_orders. compare_relations показывает in_a_not_b: 100, in_b_not_a: 0. Какая последовательность investigation?

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

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

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

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