dbt Core: Data Transformation, Models & Testing Guide
dbt (data build tool) is the analytics engineering standard for transforming data inside your warehouse. Write modular SQL, test every column, generate documentation automatically, and ship production-quality data models with confidence.
What is dbt?
dbt (data build tool) is an open-source command-line tool that enables analytics engineers to transform data in their warehouse using plain SQL. It handles only the T in ELT — data must already be in the warehouse via a loader. dbt compiles your SELECT-based models into DDL/DML statements, executes them in the correct dependency order, tests the output, and generates a documentation site with full lineage — all from your SQL and YAML configuration files.
dbt works with all major warehouses: Snowflake, BigQuery, Redshift, Databricks, DuckDB, and Postgres. A typical dbt project contains staging models that clean raw source data, intermediate models that join and enrich, and marts (fact and dimension tables) that serve BI tools. Version control, peer review, and CI testing of data transformations become standard practice.
Project Structure
# dbt project layout my_project/ ├── dbt_project.yml # Project config (name, version, model paths) ├── profiles.yml # Connection credentials (usually in ~/.dbt/) ├── models/ │ ├── staging/ # stg_* models: clean raw sources │ │ ├── schema.yml # Source + model definitions and tests │ │ └── stg_orders.sql │ ├── intermediate/ # int_* models: business logic joins │ └── marts/ # fct_* and dim_* models: final outputs ├── tests/ # Singular SQL tests (return failing rows) ├── macros/ # Reusable Jinja macros ├── snapshots/ # SCD Type 2 change tracking ├── seeds/ # CSV files loaded as reference tables └── analyses/ # Ad-hoc SQL (not materialized)
SQL Models with ref() and source()
-- models/staging/stg_orders.sql
-- ref() creates a dependency on the source model
WITH source AS (
SELECT * FROM {{ source('raw', 'orders') }}
),
renamed AS (
SELECT
id AS order_id,
user_id,
status,
amount / 100.0 AS amount_usd, -- convert cents
created_at::TIMESTAMP AS created_at
FROM source
WHERE status != 'cancelled'
)
SELECT * FROM renamed;
-- models/marts/fct_orders.sql
WITH orders AS (
SELECT * FROM {{ ref('stg_orders') }}
),
customers AS (
SELECT * FROM {{ ref('stg_customers') }}
),
final AS (
SELECT
o.order_id,
o.amount_usd,
o.created_at,
c.email,
c.country
FROM orders o
LEFT JOIN customers c USING (user_id)
)
SELECT * FROM final; Schema Tests & Source Freshness
# models/staging/schema.yml — generic tests
version: 2
models:
- name: stg_orders
description: "Cleaned orders from the raw source"
columns:
- name: order_id
description: "Primary key"
tests:
- unique
- not_null
- name: status
tests:
- accepted_values:
values: ['placed', 'shipped', 'delivered', 'returned']
- name: amount_usd
tests:
- not_null
- dbt_utils.accepted_range:
min_value: 0
max_value: 100000
sources:
- name: raw
database: analytics
schema: raw_data
tables:
- name: orders
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
loaded_at_field: _etl_loaded_at Incremental Models & CLI Commands
-- models/marts/fct_events_incremental.sql
{{
config(
materialized='incremental',
unique_key='event_id',
on_schema_change='sync_all_columns'
)
}}
SELECT
event_id,
user_id,
event_type,
properties,
occurred_at
FROM {{ source('raw', 'events') }}
{%- if is_incremental() %}
-- Only process rows newer than the latest in the table
WHERE occurred_at > (SELECT MAX(occurred_at) FROM {{ this }})
{%- endif %};
-- Run commands
-- dbt run # run all models
-- dbt run --select stg_orders # run one model
-- dbt test # run all tests
-- dbt docs generate && dbt docs serve # open docs
-- dbt source freshness # check source freshness dbt vs Stored Procedures vs Custom Python ETL
| Feature | dbt Core | Stored Procedures | Custom Python ETL |
|---|---|---|---|
| Version control | Git-native | Manual / DDL scripts | Git (code only) |
| Testing | Built-in (schema.yml) | None / manual | Unit tests (pytest) |
| Documentation | Auto-generated with lineage | None by default | Manual |
| Dependency resolution | Automatic via ref() | Manual ordering | Manual / Airflow DAG |
| Dev/prod isolation | Built-in (profiles) | Manual schema swaps | Env vars |
| Non-SQL logic | Limited (Python models) | Procedural SQL only | Full Python flexibility |
Best Practices
- Follow the staging → intermediate → marts layer pattern; never join raw sources directly in marts
- Always define sources in schema.yml — never reference raw tables with hardcoded names
- Add at minimum
uniqueandnot_nulltests on all primary keys - Use incremental models for large event tables, full table refreshes for smaller dimensions
- Run
dbt docs generatein CI so documentation is always current - Use the
--selectflag and state-based CI to test only changed models in pull requests
Frequently Asked Questions
What does dbt actually do?▼
dbt (data build tool) handles the T in ELT — it transforms data that already lives in your data warehouse. You write SELECT statements in SQL files (models), and dbt compiles them into CREATE TABLE AS / CREATE VIEW AS statements and executes them in the warehouse. dbt does not move data into the warehouse — that is the job of a data loader like Fivetran or Airbyte. dbt handles dependency resolution between models, schema documentation, data testing, and incremental loading patterns.
What is the ref() function and why is it important?▼
The ref() function is how models declare dependencies on each other. When you write {{ ref('stg_orders') }}, dbt resolves it to the correct database.schema.table name for your environment (dev vs prod) and records the dependency in the DAG. This means dbt can run models in the correct order, and changing a model name only requires updating the ref() calls. Without ref(), you would hardcode table names and lose both the dependency graph and environment isolation.
What is the difference between a view, table, and incremental materialization?▼
A view runs the SQL query every time it is queried — lightweight but slow for complex queries. A table runs the SQL and stores the results as a physical table, rebuilt completely on every dbt run. An incremental model only processes new or updated rows on subsequent runs, appending or upserting into the existing table. Use views for lightweight staging models, tables for marts and frequently queried models, and incremental for large event or fact tables where full rebuilds are too slow or expensive.
What are dbt tests and how do they work?▼
dbt has two types of tests. Generic tests are configured in schema.yml files and check things like unique, not_null, accepted_values, and relationships across models — dbt ships four built-in tests and packages like dbt-utils add more. Singular tests are custom SQL files in the tests/ directory that return failing rows — any rows returned means the test fails. Run dbt test to execute all tests after dbt run. Tests are essential for catching data quality issues in the pipeline before downstream users query bad data.
How does dbt handle environments (dev vs prod)?▼
dbt uses profiles (defined in ~/.dbt/profiles.yml) to separate environments. Each environment points to a different schema or database — for example, dev writes to analytics_dev and prod writes to analytics. The ref() and source() macros automatically resolve to the correct location based on the target environment. This means every developer has an isolated sandbox to test changes without affecting production tables. CI/CD pipelines run dbt in a slim CI mode, testing only changed models using state comparison.
What is the difference between dbt Core and dbt Cloud?▼
dbt Core is the open-source CLI tool that runs locally or in CI/CD pipelines — it is free and handles all core transformation, testing, and documentation functionality. dbt Cloud is the hosted service from dbt Labs that adds a browser-based IDE, managed job scheduling, CI/CD integration, centralized logging, the dbt Explorer for lineage visualization, and SSO. dbt Core is sufficient for teams with existing orchestration (Airflow, Prefect). dbt Cloud is useful for teams that want a managed experience without building their own orchestration.
Related Products
on GumroadDeveloper Cheat Sheet Bundle
10 print-ready cheat sheets for the tools developers use every day: Git, Docker, Linux CLI, SQL, Regex, HTTP Status codes, VS Code shortcuts, Bash scripting, jq/JSON, and Markdown.
Buy on Gumroad →Developer Productivity Bundle
Stop rebuilding the same setup every project. 51 VSCode snippets, 40 Git aliases, 5 Docker Compose stacks, 5 GitHub Actions workflows, 5 README templates, and a dotfiles kit. Set up a professional dev environment in under an hour.
Buy on Gumroad →AI Prompt Engineering Toolkit
58 structured prompt templates for Claude, ChatGPT, and Gemini — organized by use case. Code Review, Debugging, Documentation, Architecture, and 10 model-specific tips. Stop writing prompts from scratch.
Buy on Gumroad →You Might Also Need
SQL Formatter & Beautifier
Format and beautify SQL queries online. Auto-indent SELECT, FROM, WHERE, JOIN clauses, uppercase keywords, and minify SQL to a single line. Supports MySQL, PostgreSQL, SQLite. Free, runs in browser —
Open free tool →
SQL Query Formatter
Format and prettify SQL queries online. Auto-indent SELECT, FROM, WHERE, JOIN clauses, uppercase or lowercase keywords, configurable indent size. Supports MySQL, PostgreSQL, SQLite. Runs in browser.
Open free tool →
SQL Query Formatter Pro
Advanced SQL formatter with MySQL, PostgreSQL, and SQLite dialect support. Syntax highlighting, configurable indent size and keyword case. Free, runs in your browser.
Open free tool →
More Free Tools
Related Articles
dbt Complete Guide 2026: Models, Tests, Macros & CI/CD
dbt (data build tool) complete guide 2026: SQL models, ref() function, sources, generic/singular tests, macros with Jinja2, incremental models, dbt Cloud, and CI/CD integration.
ETL vs ELT: Modern Data Pipeline Architecture Guide 2026
ETL vs ELT data pipeline comparison 2026: traditional ETL limitations, cloud ELT advantages, dbt for transformation, tools comparison (Fivetran/Airbyte/dbt), and when to use each approach.
Database Indexing Strategies Every Developer Must Know
Master database indexing in 2026. Learn B-tree, hash, composite, partial, and covering indexes with practical SQL examples for PostgreSQL and MySQL.
PostgreSQL vs MySQL: Which to Choose in 2026?
Comprehensive comparison of PostgreSQL and MySQL in 2026. Performance, JSON support, replication, licensing, and a decision framework for choosing the right database for your project.
PostgreSQL vs MySQL 2026: Which Database Should You Choose?
A comprehensive comparison of PostgreSQL and MySQL in 2026. Deep dive into performance, JSON support, replication, ecosystem, and which database fits your use case.
Enjoying this tool? Get more like it.
Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.
Unlock AI-Powered Dev Tools
- ⚡ AI Code Review, Doc Generator & SQL Builder
- ⚡ All premium templates & early access
- ⚡ Member discounts on Gumroad products
$9/mo after trial · Cancel anytime
Want the full toolkit?
Get DevPlaybook Pro
Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.
MIT licensed · Instant download · No subscription
See what's included → Browse all deals →