Files
magnus919_agent-skills/data-engineering/references/dbt-patterns.md
T
Magnus HedemarkandGitHub c7c4d3b74f Port 11 methodology skills from hermes-profiles (#69)
Engineering: backend-engineering, frontend-engineering, data-engineering,
ml-engineering, platform-engineering, qa-methodology

Executive: go-to-market, legal-strategy, operational-design, org-design,
product-strategy

ml-engineering: added missing training-infrastructure.md reference
qa-methodology: added test-data-management, performance-testing,
security-testing references

All frontmatter converted to agent-skills convention.
Source: https://github.com/magnus919/hermes-profiles
2026-07-21 00:58:26 -04:00

1170 lines
36 KiB
Markdown

# dbt (Data Build Tool) — Comprehensive Reference Guide
> A methodology reference for data-engineering teams adopting dbt as the transformation layer in the modern data stack.
---
## Table of Contents
1. [What Is dbt and What Problem Does It Solve?](#1-what-is-dbt-and-what-problem-does-it-solve)
2. [dbt Core vs dbt Cloud](#2-dbt-core-vs-dbt-cloud)
3. [dbt Project Structure](#3-dbt-project-structure)
4. [dbt Modeling Concepts (Kimball Star Schema)](#4-dbt-modeling-concepts-kimball-star-schema)
5. [dbt Materializations](#5-dbt-materializations)
6. [dbt Tests](#6-dbt-tests)
7. [dbt Sources and Source Freshness](#7-dbt-sources-and-source-freshness)
8. [dbt Snapshots (Slowly Changing Dimensions)](#8-dbt-snapshots-slowly-changing-dimensions)
9. [dbt Documentation Generation](#9-dbt-documentation-generation)
10. [dbt Jinja/SQL Templating and Macros](#10-dbt-jinjasql-templating-and-macros)
11. [dbt Packages (dbt_utils, dbt_expectations)](#11-dbt-packages)
12. [dbt CI/CD Integration Patterns](#12-dbt-cicd-integration-patterns)
13. [dbt Mesh / Multi-Project Deployments](#13-dbt-mesh--multi-project-deployments)
---
## 1. What Is dbt and What Problem Does It Solve?
**dbt (data build tool)** is an open-source command-line tool and platform that enables analytics engineers and data analysts to transform data in their warehouse using SQL `SELECT` statements. It applies software-engineering best practices — version control, modularity, testing, CI/CD, documentation — to the data transformation layer.
### The Core Problem
Before dbt, the typical data workflow looked like:
1. Raw data lands in a warehouse via EL(E) tools (Fivetran, Airbyte, Stitch).
2. Transformations are written as arbitrary Python scripts, stored procedures, or tangled SQL in BI tools.
3. There is no lineage tracking, no testing, no documentation, and no repeatable deployment process.
4. Collaboration is hard because transformations are ad-hoc, not modular.
**dbt solves this by:**
- Moving the **T** (transform) from ETL to ELT — transformations happen *inside* the warehouse after data is loaded.
- Providing a **declarative, modular** framework: you write SQL `SELECT` statements, and dbt handles DDL (`CREATE TABLE`, `CREATE VIEW`, `INSERT`, `MERGE`) automatically.
- **Inferring a DAG** (directed acyclic graph) from `ref()` calls between models, enabling automatic dependency resolution and execution ordering.
- Bringing **software engineering to data**: version control (git), testing, documentation, CI/CD, package management.
### Key Concepts
| Concept | Description |
|---|---|
| **Models** | SQL files that `SELECT` from sources or other models; dbt materializes them as views/tables/incremental builds |
| **Tests** | Assertions on data quality — uniqueness, not-null, referential integrity, custom logic |
| **Sources** | Declarations of raw database tables loaded by EL tools; enables lineage, freshness checks |
| **Snapshots** | Type-2 slowly changing dimension (SCD) recording |
| **Seeds** | CSV files loaded into the warehouse as tables (for small reference/lookup data) |
| **Exposures** | Declarations of downstream consumers (dashboards, apps, ML models) |
| **Metrics** | Business metric definitions used by the dbt Semantic Layer |
> dbt is *not* an EL tool — it does not extract or load data. It assumes data already exists in a data warehouse (Snowflake, BigQuery, Redshift, Databricks, Postgres, etc.).
---
## 2. dbt Core vs dbt Cloud
### dbt Core
- **Free and open-source** (Apache 2.0 license).
- Command-line tool: `pip install dbt-core` + adapter for your warehouse (`dbt-snowflake`, `dbt-bigquery`, etc.).
- Requires you to manage your own orchestration (Airflow, Dagster, cron, GitHub Actions, etc.).
- No web UI — all development happens in a code editor + CLI.
- Community-driven; no official scheduling, logging, or collaboration features.
### dbt Cloud
- **Managed SaaS platform** by dbt Labs.
- Includes a web-based IDE, job scheduler, run history, and alerting.
- Built-in CI/CD via "Compare Changes" and environment promotion.
- **dbt Semantic Layer** with GraphQL and JDBC APIs for BI tool integration.
- **dbt Mesh** support for cross-project collaboration (multi-project `ref`).
- Role-based access control (RBAC), audit logs, SSO (Enterprise).
- **Pricing** is usage-based (by model runs/credits); free Developer tier available.
### Decision Matrix
| Criteria | dbt Core | dbt Cloud |
|---|---|---|
| Cost | Free | Paid (metered) |
| Orchestration | External (Airflow, Dagster, etc.) | Built-in scheduler |
| UI | CLI only | Web IDE + CLI |
| CI/CD | Manual setup (CI runner) | Built-in (Compare Changes) |
| Semantic Layer | Not available | Included (all paid tiers) |
| dbt Mesh | Limited (dbt-loom, manual) | Native support |
| Multi-user Dev | Git-based only | Managed environments + RBAC |
| Support | Community | Vendor support tiers |
**Typical pattern:** Teams using dbt Core locally for development and dbt Cloud (or a self-hosted orchestration tool) for production execution. Some teams use Core exclusively with Airflow/Dagster.
---
## 3. dbt Project Structure
A standard dbt project created via `dbt init <project_name>` has this layout:
```
my_dbt_project/
├── .gitignore
├── README.md
├── dbt_project.yml # Project config (name, profile, model paths, etc.)
├── profiles.yml # (outside project dir, ~/.dbt/) — DB connection config
├── models/ # SQL models (the core of the project)
│ ├── staging/ # Raw → cleaned, one-to-one with source tables
│ │ ├── _stg__models.yml # schema/docs for staging models
│ │ ├── stg_customers.sql
│ │ └── stg_orders.sql
│ ├── intermediate/ # Business-logic transformations between staging and marts
│ │ ├── int_order_items.sql
│ │ └── ...
│ └── marts/ # Business-facing models (facts + dimensions)
│ ├── marketing/
│ ├── finance/
│ └── ...
├── tests/ # Singular tests (ad-hoc SQL assertions)
│ ├── assert_total_revenue_positive.sql
│ └── ...
├── macros/ # Jinja macros for reusable SQL logic
│ ├── generate_schema_name.sql
│ └── ...
├── snapshots/ # Type-2 SCD snapshots
│ ├── scd_customers.sql
│ └── ...
├── seeds/ # CSV files loaded as tables
│ ├── country_codes.csv
│ └── ...
├── analyses/ # Ad-hoc queries (not materialized)
│ └── ...
└── data/ # (deprecated in favor of seeds/)
```
### Key Files
**`dbt_project.yml`** — The project manifest:
```yaml
name: my_project
version: "1.0.0"
config-version: 2
profile: my_project_profile # references profiles.yml
model-paths: ["models"]
seed-paths: ["seeds"]
test-paths: ["tests"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]
clean-targets:
- "target"
- "dbt_packages"
models:
my_project:
staging:
+materialized: view
intermediate:
+materialized: view
marts:
+materialized: table
```
### Node Types in Detail
| Node Type | Directory | Description |
|---|---|---|
| **Models** | `models/` | SQL `SELECT` statements materialized as views/tables |
| **Sources** | Defined in YAML (inside `models/`) | Declare upstream raw tables for lineage and freshness |
| **Tests** | `tests/` (singular) + YAML `tests:` blocks (generic) | Data quality assertions |
| **Snapshots** | `snapshots/` | SCD Type-2 tracking |
| **Seeds** | `seeds/` | Small CSV lookup tables |
| **Exposures** | Defined in YAML | Declare downstream consumers (dashboard URLs, etc.) |
| **Metrics** | Defined in YAML | Business metric definitions for the Semantic Layer |
| **Analyses** | `analyses/` | SQL that is *not* materialized (ad-hoc exploration) |
---
## 4. dbt Modeling Concepts (Kimball Star Schema)
The gold standard for dbt projects is the **Kimball dimensional modeling** approach organized into a **layered architecture**:
```
┌─────────────────────────────────────────────────┐
│ Raw Data (EL layer — Fivetran, Airbyte, etc.) │
│ Tables in warehouse: order_db.orders, etc. │
└────────────────────┬────────────────────────────┘
│ source()
┌─────────────────────────────────────────────────┐
│ Staging Layer (stg_*) │
│ - One model per source table │
│ - Light cleaning: rename, cast, deduplicate │
│ - No joins — 1:1 with source │
│ - Materialized as VIEW │
└────────────────────┬────────────────────────────┘
│ ref()
┌─────────────────────────────────────────────────┐
│ Intermediate Layer (int_*) │
│ - Business-logic transformations │
│ - Joins across staging models │
│ - Pivot/unpivot, aggregations, filtering │
│ - Usually VIEW (or ephemeral CTE) │
└────────────────────┬────────────────────────────┘
│ ref()
┌─────────────────────────────────────────────────┐
│ Mart Layer (fct_*, dim_*) │
│ - Facts: measures, foreign keys, grain-defining │
│ - Dimensions: descriptive attributes, conformed │
│ - Materialized as TABLE or INCREMENTAL │
└────────────────────┬────────────────────────────┘
Dashboards / Exposures
```
### Staging Models (`stg_*`)
Purpose: clean, type, and rename raw data. Always 1:1 with a source table.
```sql
-- models/staging/stg_orders.sql
WITH source AS (
SELECT * FROM {{ source('source_name', 'orders') }}
),
renamed AS (
SELECT
id AS order_id,
customer_id AS customer_id,
order_date AS order_date,
status AS order_status,
amount AS order_amount,
-- standard timestamp
_loaded_at AS loaded_at
FROM source
WHERE id IS NOT NULL
)
SELECT * FROM renamed
```
### Intermediate Models (`int_*`)
Purpose: bridge staging → marts. Common patterns:
- **Pivots**: `int_orders_pivoted` — pivot order statuses into columns
- **Aggregations**: `int_customer_orders` — aggregate orders per customer
- **Joins**: `int_order_items_joined` — join orders to line items
```sql
-- models/intermediate/int_customer_orders.sql
SELECT
customer_id,
MIN(order_date) AS first_order_date,
MAX(order_date) AS most_recent_order_date,
COUNT(order_id) AS number_of_orders,
SUM(order_amount) AS lifetime_value
FROM {{ ref('stg_orders') }}
GROUP BY customer_id
```
### Fact Models (`fct_*`)
- Represent business processes/events (sales, orders, clicks, shipments).
- Contain measures (numeric, additive) and foreign keys to dimensions.
- Grain must be explicitly stated in YAML documentation.
```sql
-- models/marts/fct_orders.sql
SELECT
order_id,
customer_id,
order_date,
order_amount,
order_status
FROM {{ ref('stg_orders') }}
```
### Dimension Models (`dim_*`)
- Represent business entities (customer, product, date, store).
- Contain descriptive attributes.
- Are *conformed* (same attributes mean the same thing across facts).
```sql
-- models/marts/dim_customers.sql
SELECT
customer_id,
first_name || ' ' || last_name AS customer_name,
email,
city,
country,
first_order_date,
most_recent_order_date,
number_of_orders,
lifetime_value
FROM {{ ref('int_customer_orders') }}
```
### Best Practice: Directory Layout Inside `marts/`
```
models/
marts/
marketing/
dim_customers.sql
fct_customer_attribution.sql
finance/
fct_orders.sql
dim_products.sql
product/
fct_sessions.sql
dim_products.sql # shared (conformed)
```
Each mart subdirectory gets its own `_models.yml` file for schema/documentation.
---
## 5. dbt Materializations
Materializations determine *how* a model is physically built in the warehouse.
### View (default)
```sql
{{ config(materialized='view') }}
SELECT ...
```
- Creates a `CREATE VIEW AS ...`.
- **Pros**: always up-to-date, no storage cost, fast to create.
- **Cons**: slower to query (especially with nested views), can't add indexes/partitions.
- **Use for**: staging and intermediate models.
### Table
```sql
{{ config(materialized='table') }}
SELECT ...
```
- Creates `CREATE TABLE AS SELECT` (full refresh every run).
- **Pros**: fast queries, can be indexed/clustered.
- **Cons**: expensive to rebuild fully each run, requires storage.
- **Use for**: small-to-medium marts, dimensions.
### Incremental
```sql
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge' -- or 'insert_overwrite', 'delete+insert'
) }}
SELECT ...
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
```
- Only processes new/changed rows since the last run.
- Strategies: `merge` (Snowflake, Databricks, BigQuery), `insert_overwrite` (BigQuery partitions), `delete+insert` (Redshift, Postgres).
- **Pros**: efficient for large-volume append or upsert workloads.
- **Cons**: more complex, risk of data drift if `unique_key` is wrong or source data is mutated outside incremental window.
- **Use for**: large fact tables, event logs, transaction tables.
### Ephemeral
```sql
{{ config(materialized='ephemeral') }}
SELECT ...
```
- Not materialized at all — becomes a CTE (common table expression) wherever it's `ref()`'d.
- **Pros**: no storage, zero maintenance.
- **Cons**: can't be directly queried, can cause deeply nested CTEs.
- **Use for**: lightweight intermediate transformations that are only used once.
### Comparison
| Materialization | DDL | Storage | Query Speed | Refresh |
|---|---|---|---|---|
| **view** | `CREATE VIEW` | None | Slow | Always live |
| **table** | `CREATE TABLE AS` | Full | Fast | Full refresh |
| **incremental** | `MERGE` / `INSERT` | Full | Fast | Incremental |
| **ephemeral** | None | None | Depends | N/A (CTE) |
---
## 6. dbt Tests
dbt provides a testing framework to assert data quality. Tests are run via `dbt test`.
### Generic Tests (schema tests)
Defined in YAML — reusable assertions against columns:
```yaml
# models/marts/_models.yml
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- unique
- not_null
- name: email
tests:
- unique
- not_null
- name: country
tests:
- accepted_values:
values: ['US', 'UK', 'DE', 'FR', 'CA']
- name: fct_orders
columns:
- name: customer_id
tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
```
**Built-in generic tests:**
- `unique` — no duplicate values in column
- `not_null` — no NULL values
- `accepted_values` — column values come from a defined list
- `relationships` — referential integrity (foreign key check)
- Custom ones from packages: `dbt_utils.expression_is_true`, `dbt_expectations.expect_column_values_to_match_regex`, etc.
### Singular Tests (data tests)
Standalone SQL files in `tests/` that return failing rows. Any returned row == test failure.
```sql
-- tests/assert_positive_revenue.sql
SELECT
order_id,
order_amount
FROM {{ ref('fct_orders') }}
WHERE order_amount < 0
```
### Custom Generic Tests (test macros)
Create reusable test macros in `macros/tests/`:
```sql
{% test assert_positive(model, column_name) %}
SELECT *
FROM {{ model }}
WHERE {{ column_name }} < 0
{% endtest %}
```
Then use it in YAML:
```yaml
tests:
- assert_positive
```
### Running Tests
```bash
dbt test # run all tests
dbt test --select dim_customers # test a single model
dbt test --select tag:nightly # tests tagged 'nightly'
```
**Store test failures:**
```bash
dbt test --store-failures # persists failures as tables for review
```
### Test Severity (dbt v1.5+)
```yaml
tests:
- not_null:
severity: warn # non-blocking; reported but doesn't fail the run
```
---
## 7. dbt Sources and Source Freshness
### Declaring Sources
Sources define which raw database tables your pipeline starts from.
```yaml
# models/staging/_sources.yml
version: 2
sources:
- name: jaffle_shop # logical name
database: raw_db
schema: public
tables:
- name: orders
description: "Raw orders from the jaffle_shop transactional system"
loaded_at_field: _etl_loaded_at
freshness:
warn_after: { count: 12, period: hour }
error_after: { count: 24, period: hour }
columns:
- name: id
description: Primary key
tests:
- unique
- not_null
- name: customers
loaded_at_field: _etl_loaded_at
freshness:
warn_after: { count: 24, period: hour }
```
### Using Sources in Models
```sql
-- models/staging/stg_orders.sql
SELECT *
FROM {{ source('jaffle_shop', 'orders') }}
```
Using `source()` instead of raw table names gives you:
- **Lineage**: dbt tracks dependencies from models back to source tables.
- **Freshness**: `dbt source freshness` runs timestamp-based checks to detect stale data.
### Source Freshness Command
```bash
dbt source freshness
```
Output: a JSON file `target/sources.json` with per-source freshness results. This can be integrated into monitoring/alerting pipelines. Failures can be flagged as warnings or errors.
### Snapshotting Source Config
In `dbt_project.yml` you can set a blanket source freshness policy:
```yaml
sources:
jaffle_shop:
freshness:
warn_after: { count: 6, period: hour }
loaded_at_field: _loaded_at
```
---
## 8. dbt Snapshots (Slowly Changing Dimensions)
Snapshots implement **Type 2 Slowly Changing Dimensions (SCD)** — they track historical changes to dimension attributes.
### How Snapshots Work
1. You define a snapshot SQL file in `snapshots/` that `SELECT`s the source data.
2. dbt compares the current source data against the existing snapshot table.
3. If any tracked column changed, dbt **closes** the old row (sets `dbt_valid_to`) and **inserts** a new row (sets `dbt_valid_from`).
### Snapshot Configuration
```sql
-- snapshots/scd_customers.sql
{% snapshot scd_customers %}
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='check',
check_cols='all' -- or ['email', 'city', 'country']
)
}}
SELECT * FROM {{ source('jaffle_shop', 'customers') }}
{% endsnapshot %}
```
### Snapshot Strategies
| Strategy | Description |
|---|---|
| **`timestamp`** | Uses a `updated_at` column to detect changes (more efficient). Requires `updated_at` column. |
| **`check`** | Compares specified columns (or all columns) for changes. No timestamp needed. |
**Timestamp strategy (preferred when possible):**
```sql
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='timestamp',
updated_at='updated_at',
invalidate_hard_deletes=True
)
}}
```
### Snapshot Metadata Columns
Every snapshot row gets these columns automatically:
| Column | Meaning |
|---|---|
| `dbt_scd_id` | Surrogate key for the SCD record |
| `dbt_updated_at` | When the row was updated (the `updated_at` value or snapshot run time) |
| `dbt_valid_from` | Start date/time of this version |
| `dbt_valid_to` | End date/time (NULL = current version) |
| `dbt_is_contaminated` | Flag if multiple changes happened between snapshot runs (unusual) |
### Querying Snapshot Tables
```sql
-- Get current customers
SELECT * FROM snapshots.scd_customers WHERE dbt_valid_to IS NULL
-- Get customers as of a specific date
SELECT * FROM snapshots.scd_customers
WHERE '2024-06-01' BETWEEN dbt_valid_from AND COALESCE(dbt_valid_to, '9999-12-31')
-- Full history for a specific customer
SELECT * FROM snapshots.scd_customers
WHERE customer_id = 42
ORDER BY dbt_valid_from
```
---
## 9. dbt Documentation Generation
dbt can auto-generate a static documentation site from your project using `dbt docs generate`.
### What Gets Generated
- **Model lineage** (DAG visualization) via `dbt docs serve` (interactive web UI).
- **Schema/datatype info** from the warehouse (via `dbt docs generate` which runs `dbt run` + `dbt test` + catalog collection).
- **Descriptions** from YAML schema files.
- **Test results** and sources information.
### Adding Documentation
```yaml
# models/marts/_models.yml
version: 2
models:
- name: dim_customers
description: >
Customer dimension table. One row per customer with current attributes
and aggregated lifetime metrics.
columns:
- name: customer_id
description: "Primary key from the source CRM system"
tests:
- unique
- not_null
- name: lifetime_value
description: "Total revenue from this customer (all orders)"
```
### Docs Blocks (reusable markdown)
```sql
-- models/docs.md
{% docs dim_customers_description %}
The **customer dimension** contains one row per customer.
It includes:
- Demographics (name, email, location)
- Behavioral metrics (first/last order date, lifetime value)
{% enddocs %}
```
Referenced in YAML:
```yaml
models:
- name: dim_customers
description: "{{ doc('dim_customers_description') }}"
```
### Generating and Serving
```bash
dbt docs generate # produces target/catalog.json + target/manifest.json
dbt docs serve # serves docs at http://localhost:8080
dbt docs serve --port 8081
```
### CI Integration
Many teams upload the generated docs to a static hosting service (S3, Netlify, GitHub Pages) as part of CI/CD, so the documentation is always up-to-date with production.
---
## 10. dbt Jinja/SQL Templating and Macros
dbt uses **Jinja** (Python templating engine) to make SQL programmable.
### Basic Jinja in dbt
```sql
SELECT
order_id,
{% if include_customer_name %}
customer_name,
{% endif %}
order_amount * {{ multiplier }} AS adjusted_amount
FROM {{ ref('fct_orders') }}
```
### Built-in Jinja Functions
| Function | Purpose |
|---|---|
| `{{ ref('model_name') }}` | Reference another model (creates DAG edge) |
| `{{ source('source_name', 'table') }}` | Reference a declared source |
| `{{ config(...) }}` | Set model-level configuration |
| `{{ this }}` | Current model's database object reference |
| `{{ is_incremental() }}` | Returns `True` if the model is doing an incremental run |
| `{{ var('variable_name') }}` | Access user-defined variables |
| `{{ env_var('ENV_NAME') }}` | Access environment variables |
### Macros
Macros are reusable Jinja-SQL snippets, stored in `macros/`. They are like functions.
**Creating a macro:**
```sql
{# macros/cents_to_dollars.sql #}
{% macro cents_to_dollars(column_name, precision=2) -%}
({{ column_name }} / 100.0)::numeric(16, {{ precision }})
{%- endmacro %}
```
**Using a macro:**
```sql
SELECT
{{ cents_to_dollars('order_amount_cents') }} AS order_amount_dollars
FROM {{ ref('stg_orders') }}
```
### Control Flow
```sql
{% if target.name == 'prod' %}
-- only run in production
AND status IN ('shipped', 'delivered')
{% elif target.name == 'dev' %}
-- sample for development
LIMIT 1000
{% endif %}
```
### Loops
```sql
{% for column in var('payment_methods') %}
SUM(CASE WHEN payment_method = '{{ column }}' THEN amount ELSE 0 END) AS {{ column }}_amount
{%- if not loop.last %},{% endif %}
{% endfor %}
```
### DBT_UTILS Macro Example
```sql
{% set payment_methods = dbt_utils.get_column_values(
table=ref('stg_payments'),
column='payment_method'
) %}
```
### Materialized Macro (Advanced)
dbt also provides *dispatcher macros* for adapter-specific SQL:
```sql
{% macro my_custom_merge() %}
{% if target.type == 'snowflake' %}
-- Snowflake MERGE syntax
{% elif target.type == 'bigquery' %}
-- BigQuery MERGE syntax
{% endif %}
{% endmacro %}
```
### Best Practices for Macros
- Keep macros in `macros/`, organized by domain (`macros/pricing/`, `macros/logging/`).
- Prefix macros with a package name when distributing (`my_package::macro_name`).
- Document macro arguments with `{% docs %}` blocks.
- Avoid excessive Jinja complexity — it makes SQL harder to read and debug.
---
## 11. dbt Packages
dbt packages are reusable libraries of models, macros, and tests. They are managed via a `packages.yml` file.
### Installing Packages
```yaml
# packages.yml
packages:
- package: dbt-labs/dbt_utils
version: 1.1.1
- package: calogica/dbt_expectations
version: 0.9.0
- package: dbt-labs/spark_utils
version: 0.3.0
- git: "https://github.com/dbt-labs/dbt-utils.git"
revision: 0.9.2 # optional
```
Install with:
```bash
dbt deps
```
Packages are installed into the `dbt_packages/` directory.
### dbt_utils (dbt-labs/dbt_utils)
The most widely used dbt package. Key capabilities:
**Cross-database macros:**
| Macro | Purpose |
|---|---|
| `dbt_utils.surrogate_key('col1', 'col2')` | Create a hash-based surrogate key |
| `dbt_utils.datediff('start', 'end', 'day')` | Cross-database date difference |
| `dbt_utils.date_trunc('month', 'date_col')` | Cross-database date truncation |
| `dbt_utils.hash('col')` | Cross-database hash function |
| `dbt_utils.concat(['col1', 'col2'])` | Cross-database concatenation |
**Testing macros:**
| Test | Purpose |
|---|---|
| `dbt_utils.expression_is_true` | Assert that an expression is true |
| `dbt_utils.unique_combination_of_columns` | Composite uniqueness |
| `dbt_utils.mutually_exclusive_ranges` | No overlapping ranges |
| `dbt_utils.cardinality_equality` | Two sources have same set of values |
| `dbt_utils.recency` | Max timestamp is recent enough |
**Schema/table utilities:**
| Macro | Purpose |
|---|---|
| `dbt_utils.get_column_values()` | Return list of column values |
| `dbt_utils.get_tables_by_pattern()` | Find tables matching a pattern |
| `dbt_utils.get_query_results_as_dict()` | Run any SQL return results |
**Schema tests (YAML):**
```yaml
tests:
- dbt_utils.expression_is_true:
expression: "order_amount >= 0"
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- order_id
- line_item_id
```
### dbt_expectations (calogica/dbt_expectations)
Inspired by the Python `great_expectations` library. Provides dozens of data-quality tests.
**Common tests:**
| Test | Purpose |
|---|---|
| `expect_column_values_to_match_regex` | Regex validation |
| `expect_column_values_to_be_between` | Range check |
| `expect_column_values_to_be_in_set` | Set membership |
| `expect_column_distinct_count_to_equal` | Exact distinct count |
| `expect_column_values_to_not_be_null` | Not-null (adds threshold support) |
| `expect_table_row_count_to_be_between` | Row count range |
| `expect_column_pair_values_A_to_be_greater_than_B` | Cross-column comparison |
| `expect_queried_row_count_to_be_between` | Dynamic SQL row count |
| `expect_queried_column_value_frequency_to_be_between` | Value frequency checks |
| `expect_table_columns_to_match_ordered_list` | Schema validation |
### Other Notable Packages
| Package | Purpose |
|---|---|
| `dbt-labs/audit_helper` | Compare row counts and values between two inputs |
| `dbt-labs/dbt-artifacts` | Parse dbt artifacts into warehouse tables |
| `dbt-labs/date_spine` | Generate date spines for calendar dimensions |
| `dbt-labs/codegen` | Auto-generate base models and YAML from source tables |
| `elementary-data/elementary` | Data monitoring, alerting, and observability |
| `re-data/re_data` | Data reliability and anomaly detection |
| `infinitelambda/dbt_ml` | ML preprocessing utilities in dbt |
---
## 12. dbt CI/CD Integration Patterns
### Pattern 1: dbt Cloud CI
1. Create a **Merge Request / Pull Request** on GitHub/GitLab.
2. dbt Cloud's CI job fires automatically.
3. It creates a **temporary schema** with the PR's changes.
4. Runs `dbt build --select state:modified+` to run only changed models and their downstream tests.
5. Reports results back as a PR check.
**Key commands:**
```bash
# Compare against production manifest
dbt build --select state:modified+ --defer --state target-prod/
```
Where `--defer` means "use production tables for unmodified models" and `state:modified+` selects changed models plus everything downstream.
### Pattern 2: dbt Core + GitHub Actions
```yaml
# .github/workflows/dbt-ci.yml
name: dbt CI
on:
pull_request:
branches: [main]
jobs:
dbt-ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install dbt-snowflake dbt-utils
dbt deps
- name: dbt build (CI)
env:
DBT_USER: ${{ vars.DBT_USER }}
DBT_PASSWORD: ${{ secrets.DBT_PASSWORD }}
run: |
dbt build --target ci --select state:modified+ --defer
```
### Pattern 3: dbt + Airflow
Use the `Cosmos` library (by Astronomer) to run dbt inside Airflow DAGs:
```python
from cosmos import DbtDag, ProjectConfig, ProfileConfig
from pendulum import datetime
dbt_dag = DbtDag(
project_config=ProjectConfig("/path/to/dbt_project"),
profile_config=ProfileConfig(
profile_name="my_project",
target_name="prod",
profiles_yml_filepath="/path/to/profiles.yml",
),
start_date=datetime(2024, 1, 1),
schedule="@daily",
catchup=False,
default_args={"retries": 2},
tags=["dbt"],
)
```
### Pattern 4: Slim CI (state-based)
```bash
# In production: upload manifest.json as an artifact
dbt run # full run
dbt docs generate
cp target/manifest.json target-prod/manifest.json
# In CI: download production manifest, run slim CI
dbt build --select state:modified+ --defer --state target-prod/
```
### Environment Strategy
```yaml
# dbt_project.yml
models:
+post-hook:
- "GRANT SELECT ON {{ this }} TO ROLE ANALYST_ROLE" # only in prod
- "{{ 'GRANT SELECT ON {{ this }} TO ROLE DEV_ROLE' if target.name == 'dev' else '' }}"
```
**Recommended targets:**
| Target | Purpose | Schema Suffix |
|---|---|---|
| `dev` | Individual developer | `_dev_<username>` |
| `ci` | PR validation | `_pr_<number>` |
| `staging` | Pre-production | `_staging` |
| `prod` | Production | (none) |
---
## 13. dbt Mesh / Multi-Project Deployments
dbt Mesh is dbt Labs' solution for scaling dbt across multiple teams and domains, enabling **decentralized ownership with centralized governance**.
### The Problem dbt Mesh Solves
- A single monolithic dbt project becomes unwieldy at scale (1000+ models, 10+ teams).
- Teams need to own their data independently but depend on models from other teams.
- No cross-project visibility or contracts between teams.
### Key Concepts
| Concept | Description |
|---|---|
| **Multi-project collaboration** | Different teams maintain separate dbt repos/projects |
| **Cross-project `ref`** | Use `ref('model_name')` across projects via `dependencies.yml` |
| **Model contracts** | Enforced column names, types, and constraints on public models |
| **Access control** | `public` / `protected` / `private` model access modifiers |
| **Versioning** | Semantic versioning for model contracts |
| **Discovery API** | Query metadata across all projects |
### Setting Up dbt Mesh
**Producer (upstream) project:**
```yaml
# models/_models.yml
models:
- name: dim_customers
access: public # can be used by other projects
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: int
constraints: [not_null, unique]
- name: customer_name
data_type: varchar(256)
- name: email
data_type: varchar(256)
```
**Consumer (downstream) project:**
```yaml
# dependencies.yml
packages:
- name: upstream_core
version: 1.0.0
# For dbt Cloud:
# (handled via Project Dependencies UI)
# For dbt Core (with dbt-loom or similar):
git: "https://github.com/team-a/dbt-core-project.git"
```
```sql
-- models/marts/fct_orders.sql
SELECT *
FROM {{ ref('upstream_core', 'dim_customers') }} -- cross-project ref
```
### Model Contracts
Contracts enforce a "schema on write" for downstream consumers:
```yaml
models:
- name: dim_customers
config:
contract:
enforced: true # dbt will fail if the model's SQL doesn't match the declared columns
columns:
- name: customer_id
data_type: int
constraints:
- type: not_null
- type: primary_key
- name: email
data_type: varchar(256)
```
### Benefits
- **Team autonomy**: Each team manages their own dbt project, CI/CD, and deployments.
- **Governance**: Model contracts prevent breaking changes across teams.
- **Scalability**: Reduced DAG complexity per project, faster CI, independent deploy cycles.
- **Reusability**: Shared domain models (e.g., `dim_customers`, `dim_dates`) are versioned and consumed by many projects.
### Tools for Multi-Project Without dbt Cloud
| Tool | Description |
|---|---|
| **dbt-loom** | Open-source CLI tool for cross-project `ref()` resolution in dbt Core |
| **dbt-meshify** | CLI by dbt Labs to assist splitting monolithic projects |
| **Custom scripts** | `git submodule` or multi-repo CI strategies |
---
## Appendix A: Essential dbt Commands
```bash
dbt init <project_name> # Create a new dbt project
dbt deps # Install packages from packages.yml
dbt debug # Verify warehouse connection
dbt seed # Load CSV files (seeds)
dbt run # Execute all models
dbt run --select +model_name # Run a model + its upstream dependencies
dbt run --select model_name+ # Run a model + its downstream dependents
dbt run --exclude tag:stale # Run everything except models tagged 'stale'
dbt test # Run all tests
dbt test --select model_name # Run tests only for a specific model
dbt build # seed + run + test (in one command, DAG-ordered)
dbt snapshot # Execute snapshots
dbt source freshness # Check source table freshness
dbt docs generate # Build documentation
dbt docs serve # Serve documentation locally
dbt ls # List all resources (models, tests, etc.)
dbt compile # Compile SQL without executing
dbt parse # Validate project without running anything
```
## Appendix B: YAML Schema File Pattern
Organize YAML files alongside models. Naming convention:
| File | Contains |
|---|---|
| `_sources.yml` | Source declarations |
| `_models.yml` | Model descriptions, column docs, tests |
| `_metrics.yml` | Metric definitions |
| `_exposures.yml` | Exposure declarations |
| `_macros.yml` | Macro documentation |
## Appendix C: Key Resources
- [Official dbt Documentation](https://docs.getdbt.com/)
- [dbt GitHub (dbt-core)](https://github.com/dbt-labs/dbt-core)
- [dbt_utils Package](https://github.com/dbt-labs/dbt-utils)
- [dbt_expectations Package](https://github.com/calogica/dbt-expectations)
- [dbt Discourse Community](https://discourse.getdbt.com/)
- [dbt Best Practices Guide](https://docs.getdbt.com/best-practices)
- [dbt Mesh Docs](https://docs.getdbt.com/docs/mesh)
---
*Document produced for data-engineering methodology skill reference. June 2026.*