dbt-data-modeling
MaheshAwasare/claude-skills-pro/skills/dbt-data-modeling/SKILL.md
Build a maintainable dbt project — staging/intermediate/mart layering, sources/refs, tests, macros, incremental models, snapshots, and the discipline that prevents the "1500 models, 0 tests" antipattern. Use when starting a new dbt project or refactoring one that's grown into a hairball.
Skill1 starsChanged 5 months ago
- Installs packages
What's in it
- dbt Data Modeling
- When to use
- When NOT to use
- The layering (the only thing that matters)
- sources vs refs
- Tests (the part nobody writes)
- Incremental models (when the table is huge)
- Macros (small, one purpose each)
- Snapshots (for slowly-changing dimensions)
- Project config
- CI for dbt
- Anti-patterns
- Verify it worked
---
name: dbt-data-modeling
description: Build a maintainable dbt project — staging/intermediate/mart layering, sources/refs, tests, macros, incremental models, snapshots, and the discipline that prevents the "1500 models, 0 tests" antipattern. Use when starting a new dbt project or refactoring one that's grown into a hairball.
---
# dbt Data Modeling
dbt's docs cover features. This skill covers *layout and discipline* — the parts that decide whether your warehouse stays understandable past the first 50 models.
## When to use
- New dbt project (Snowflake / BigQuery / Redshift / Postgres / DuckDB).
- Refactoring a project that's grown without layering.
- Setting up CI for dbt for the first time.
## When NOT to use
- Pure transformation jobs that don't need lineage (one-off Python scripts).
- Streaming pipelines — dbt is batch-first; use Materialize or RisingWave.
## The layering (the only thing that matters)
```
models/
staging/ # 1:1 with sources, light cleaning
__sources.yml
stg_<source>__<table>.sql
intermediate/ # business-specific reusable logic
int_<concept>__<verb>.sql
marts/ # narrow, consumer-facing
core/ # dimensional / source-of-truth
finance/ # domain-specific
marketing/
utils/ # macros only
```
| Layer | Purpose | Materialization | Naming |
|---|---|---|---|
| Staging | One model per source table; rename columns, cast types, no joins | view | `stg_<source>__<table>` |
| Intermediate | Reusable joins/aggregations | ephemeral or view | `int_<topic>__<verb>` (e.g. `int_orders__joined`) |
| Marts | Final tables consumers query | table or incremental | `dim_<entity>`, `fct_<event>`, or domain table |
**Rule:** stage everything from sources. No model outside `staging/` references `source()`; they reference `ref()` to a staging model. This makes source changes a single-file edit.
## sources vs refs
```yaml
# models/staging/__sources.yml
version: 2
sources:
- name: stripe
schema: raw_stripe
tables:
- name: customers
- name: subscriptions
- name: invoices
loaded_at_field: created_at
freshness:
warn_after: { count: 24, period: hour }
error_after: { count: 48, period: hour }
```
```sql
-- models/staging/stripe/stg_stripe__customers.sql
with raw as (
select * from {{ source('stripe', 'customers') }}
)
select
id as customer_id,
email,
created as created_at,
metadata
from raw
```
Then everything downstream references `{{ ref('stg_stripe__customers') }}`.
## Tests (the part nobody writes)
Every staging model gets at least: `unique` + `not_null` on the primary key, `accepted_values` on enums, `relationships` on foreign keys.
```yaml
# models/staging/stripe/_models.yml
version: 2
models:
- name: stg_stripe__customers
columns:
- name: customer_id
tests: [unique, not_null]
- name: email
tests: [not_null]
- name: stg_stripe__subscriptions
columns:
- name: subscription_id
tests: [unique, not_null]
- name: status
tests:
- accepted_values:
values: [trialing, active, past_due, canceled, unpaid]
- name: customer_id
tests:
- relationships:
to: ref('stg_stripe__customers')
field: customer_id
```
Run `dbt test` in CI on every PR. A failing test should block merge.
## Incremental models (when the table is huge)
```sql
-- models/marts/fct_events.sql
{{
config(
materialized='incremental',
unique_key='event_id',
incremental_strategy='merge',
on_schema_change='append_new_columns'
)
}}
select
event_id,
user_id,
event_type,
occurred_at
from {{ ref('stg_events__raw') }}
{% if is_incremental() %}
where occurred_at > (select coalesce(max(occurred_at), '1970-01-01'::timestamp) from {{ this }})
{% endif %}
```
Default to `merge` strategy (idempotent); fall back to `append` for true append-only logs. `unique_key` is mandatory or you'll get duplicates on re-run.
## Macros (small, one purpose each)
```sql
-- macros/dollars_to_paise.sql
{% macro dollars_to_paise(col) %}
cast({{ col }} * 100 as integer)
{% endmacro %}
```
Used as `{{ dollars_to_paise('amount') }}`. Macros longer than 30 lines are usually doing too much.
## Snapshots (for slowly-changing dimensions)
```sql
-- snapshots/snap_customers.sql
{% snapshot snap_customers %}
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='timestamp',
updated_at='updated_at'
)
}}
select * from {{ source('stripe', 'customers') }}
{% endsnapshot %}
```
Capture the history of customer changes (plan changes, email updates). Run nightly. Build SCD Type 2 dims off these.
## Project config
```yaml
# dbt_project.yml
name: acme_warehouse
version: 1.0.0
profile: acme
model-paths: ["models"]
analysis-paths: ["analyses"]
test-paths: ["tests"]
seed-paths: ["seeds"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]
models:
acme_warehouse:
staging: { +materialized: view, +schema: staging }
intermediate: { +materialized: ephemeral }
marts:
core: { +materialized: table, +schema: core }
finance: { +materialized: table, +schema: finance }
```
`+schema:` lets you separate output schemas per layer — analysts can grant access to `marts.core` only.
## CI for dbt
```yaml
# .github/workflows/dbt-ci.yml
on: [pull_request]
jobs:
dbt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install dbt-snowflake==1.8.* # pin
- run: dbt deps
- run: dbt build --select state:modified+ --defer --state ./target-prod
env: { DBT_PROFILES_DIR: . }
```
`state:modified+ --defer` only runs models you changed (and downstream). Saves CI time and warehouse credits.
## Anti-patterns
- **No staging layer; models reference `source()` everywhere** — one upstream rename is a global refactor.
- **No tests** — at minimum `unique`+`not_null` on every PK. Tests are the contract.
- **Marts that join other marts** — circular-feeling lineage. Marts should depend on intermediate or staging only.
- **Incremental without `unique_key`** — duplicate rows on re-run.
- **Snapshots run as part of `dbt run`** — they should be separate (`dbt snapshot`) on a different schedule.
- **All models materialized as table** — slow CI, expensive. Use views and ephemeral aggressively in upper layers.
- **Mixed naming conventions** — `Customer`, `customers`, `dim_customer`, `cust_dim`. Pick one and enforce in PR review.
- **No `--select state:modified+ --defer` in CI** — full builds on every PR are slow and expensive.
- **Source freshness not configured** — silent staleness. Fail loudly when upstream is broken.
- **Macros that mask SQL too much** — junior analysts can't read `{{ flexible_cohort_filter(...) }}` macros that hide 100 lines. Prefer plain SQL.
## Verify it worked
- [ ] `dbt build` runs cleanly on a fresh clone.
- [ ] `dbt test` reports zero failures and at least one test per staging model.
- [ ] No model in `models/intermediate` or `models/marts` calls `source()` directly.
- [ ] Marts schema has only consumer-ready tables; staging schema is hidden from analysts.
- [ ] CI runs `state:modified+ --defer` against prod manifest; PRs only rebuild what changed.
- [ ] Source freshness check fires alerts when upstream lags > 2x SLA.
- [ ] Incremental models use `unique_key`; re-running them is idempotent (no dup rows).
- [ ] At least one snapshot captures SCD-type-2 history of a key dim.
- [ ] `dbt docs generate && dbt docs serve` produces a navigable lineage graph.
- [ ] Layered schemas (`staging.`, `marts.`) — analysts only have access to marts.
More agent context in MaheshAwasare/claude-skills-pro
50 other files this repository gives its agents.
Skill
- algolia-searchskills/algolia-search/SKILL.md
- audit-tautological-testsskills/audit-tautological-tests/SKILL.md
- blame-archaeologyskills/blame-archaeology/SKILL.md
- brevo-emailskills/brevo-email/SKILL.md
- bun-runtimeskills/bun-runtime/SKILL.md
- clerk-authskills/clerk-auth/SKILL.md
- cloudflare-workersskills/cloudflare-workers/SKILL.md
- explain-this-diffskills/explain-this-diff/SKILL.md
- extract-skill-from-sessionskills/extract-skill-from-session/SKILL.md
- fastapi-productionskills/fastapi-production/SKILL.md
- find-dead-codeskills/find-dead-code/SKILL.md
- find-real-bugskills/find-real-bug/SKILL.md
- gdpr-dpiaskills/gdpr-dpia/SKILL.md
- github-actions-ciskills/github-actions-ci/SKILL.md
- graphql-relayskills/graphql-relay/SKILL.md
- grpc-servicesskills/grpc-services/SKILL.md
- hipaa-auditskills/hipaa-audit/SKILL.md
- india-dpdp-actskills/india-dpdp-act/SKILL.md
- java-8-to-21skills/java-8-to-21/SKILL.md
- jest-to-vitestskills/jest-to-vitest/SKILL.md
- kubernetes-helmskills/kubernetes-helm/SKILL.md
- mongo-to-postgresskills/mongo-to-postgres/SKILL.md
- nextjs-pages-to-appskills/nextjs-pages-to-app/SKILL.md
- node-version-upgradeskills/node-version-upgrade/SKILL.md
- opentelemetry-instrumentskills/opentelemetry-instrument/SKILL.md
- pci-dss-checklistskills/pci-dss-checklist/SKILL.md
- plan-the-rollbackskills/plan-the-rollback/SKILL.md
- python-2-to-3skills/python-2-to-3/SKILL.md
- razorpay-integrationskills/razorpay-integration/SKILL.md
- react-native-exposkills/react-native-expo/SKILL.md
- scaffold-cli-toolskills/scaffold-cli-tool/SKILL.md
- scaffold-fullstack-appskills/scaffold-fullstack-app/SKILL.md
- scaffold-go-microserviceskills/scaffold-go-microservice/SKILL.md
- scaffold-new-appskills/scaffold-new-app/SKILL.md
- scaffold-saas-starterskills/scaffold-saas-starter/SKILL.md
- sentry-monitoringskills/sentry-monitoring/SKILL.md
- shrink-this-prskills/shrink-this-pr/SKILL.md
- soc2-evidenceskills/soc2-evidence/SKILL.md
- spec-from-conversationskills/spec-from-conversation/SKILL.md
- stripe-integrationskills/stripe-integration/SKILL.md
- supabase-backendskills/supabase-backend/SKILL.md
- terraform-patternsskills/terraform-patterns/SKILL.md
- threat-modeling-strideskills/threat-modeling-stride/SKILL.md
- triage-stack-traceskills/triage-stack-trace/SKILL.md
- wcag-accessibility-auditskills/wcag-accessibility-audit/SKILL.md
- webpack-to-viteskills/webpack-to-vite/SKILL.md
- write-adrskills/write-adr/SKILL.md
- write-commit-messageskills/write-commit-message/SKILL.md
- write-pr-descriptionskills/write-pr-description/SKILL.md
- write-runbookskills/write-runbook/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

