Skip to main content

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.

✓ SQL-first transformations ✓ Built-in data testing ✓ Auto-generated docs ✓ Incremental models

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

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.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$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.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →