Data Engineering  /  dbt

๐Ÿ”„ dbt โ€” Data Build Tool 60 guides ยท updated 2026

Analytics engineering with SQL โ€” models, tests, sources, and Jinja macros that turn raw warehouse tables into trustworthy, documented data products.

Your First dbt Project

Getting dbt running for the first time takes about 30 minutes if you know what youโ€™re doing. This guide skips the theory and focuses on the concrete steps: install, configure a profile, write your first model, and see it run.


Prerequisites

Before starting, you need:

This walkthrough uses PostgreSQL for the examples, but the pattern is identical for other warehouses โ€” only the profile configuration changes.


Step 1: Install dbt

Create a virtual environment first. This keeps dbtโ€™s dependencies separate from your system Python.

Terminal window
python -m venv dbt-env
source dbt-env/bin/activate # Windows: dbt-env\Scripts\activate
pip install dbt-postgres # install core + postgres adapter
dbt --version # confirm the install worked

For other databases, replace dbt-postgres with the right adapter:


Step 2: Initialize the Project

Terminal window
dbt init my_first_project

dbt will ask you to select an adapter (matching what you installed) and then create a project directory with this structure:

my_first_project/
โ”œโ”€โ”€ dbt_project.yml # project configuration
โ”œโ”€โ”€ models/
โ”‚ โ””โ”€โ”€ example/ # sample models (you can delete these)
โ”œโ”€โ”€ tests/
โ”œโ”€โ”€ macros/
โ”œโ”€โ”€ seeds/
โ””โ”€โ”€ analyses/

Move into the project directory:

Terminal window
cd my_first_project

Step 3: Configure Your Profile

dbt connects to your database using a profiles.yml file stored at ~/.dbt/profiles.yml (not inside the project directory). This keeps credentials out of your project repository.

Create or edit ~/.dbt/profiles.yml:

my_first_project:
target: dev
outputs:
dev:
type: postgres
host: localhost
user: your_username
password: your_password
port: 5432
dbname: your_database
schema: dbt_dev
threads: 4

A few things to note:

Never commit passwords to source control. For production, use environment variables:

password: "{{ env_var('DBT_DB_PASSWORD') }}"

Test the connection:

Terminal window
dbt debug

All checks should show as OK. If you see connection errors, double-check your host, port, and credentials.


Step 4: Write Your First Model

Delete the sample models dbt created, then make your own. Create models/customers.sql:

-- models/customers.sql
WITH source AS (
SELECT * FROM {{ source('raw', 'customers') }}
),
cleaned AS (
SELECT
id AS customer_id,
LOWER(email) AS email,
first_name || ' ' || last_name AS full_name,
created_at
FROM source
WHERE email IS NOT NULL
)
SELECT * FROM cleaned

The {{ source() }} function references a raw table in your database. You define sources in a YAML file so dbt tracks their lineage:

Create models/sources.yml:

version: 2
sources:
- name: raw
schema: public # the schema where your raw tables live
tables:
- name: customers
- name: orders

Now create a downstream model that references the first one. Create models/customer_orders.sql:

-- models/customer_orders.sql
WITH customers AS (
SELECT * FROM {{ ref('customers') }}
),
orders AS (
SELECT * FROM {{ source('raw', 'orders') }}
),
final AS (
SELECT
c.customer_id,
c.full_name,
c.email,
COUNT(o.id) AS total_orders,
SUM(o.amount) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY 1, 2, 3
)
SELECT * FROM final

Step 5: Add Tests

dbt ships with four built-in tests you can add in a schema YAML file. Create models/schema.yml:

version: 2
models:
- name: customers
columns:
- name: customer_id
tests:
- unique
- not_null
- name: email
tests:
- not_null
- name: customer_orders
columns:
- name: customer_id
tests:
- not_null
- relationships:
to: ref('customers')
field: customer_id

The relationships test checks referential integrity โ€” every customer_id in customer_orders must exist in customers. This is the kind of data quality check that saves you from debugging downstream BI issues.


Step 6: Run Everything

Build models:

Terminal window
dbt run

You should see output like:

Running with dbt=1.8.x
Found 2 models, 5 tests, 1 source
1 of 2 START sql view model dbt_dev.customers ........... [RUN]
1 of 2 OK created sql view model dbt_dev.customers ....... [OK in 0.12s]
2 of 2 START sql view model dbt_dev.customer_orders ...... [RUN]
2 of 2 OK created sql view model dbt_dev.customer_orders .. [OK in 0.08s]
Finished running 2 view models in 0.31s.
PASS=2 WARN=0 ERROR=0 SKIP=0 TOTAL=2

Run tests:

Terminal window
dbt test

Run models and tests together (recommended):

Terminal window
dbt build

dbt build runs models and their tests in dependency order, stopping immediately if a test fails. This is the command youโ€™ll use in CI.


Step 7: Generate and View Documentation

Terminal window
dbt docs generate
dbt docs serve

This opens a browser with an auto-generated documentation site. It includes:


Controlling Materialization

By default, dbt creates views. You can change this per model in dbt_project.yml:

models:
my_first_project:
+materialized: view # default for the whole project
marts:
+materialized: table # tables for anything in the marts/ folder

Or in the model file itself:

{{ config(materialized='table') }}
SELECT ...

Use views for staging models (cheap, always fresh), tables for heavy transformations you donโ€™t want re-executed on every query, and incremental for large datasets where rebuilding from scratch is too expensive.


Project Structure That Scales

Once you move beyond two models, organize your project into layers:

models/
โ”œโ”€โ”€ staging/ โ† one model per source table, minimal transformation
โ”‚ โ”œโ”€โ”€ stg_customers.sql
โ”‚ โ””โ”€โ”€ stg_orders.sql
โ”œโ”€โ”€ intermediate/ โ† joins and business logic, not exposed to BI directly
โ”‚ โ””โ”€โ”€ int_customer_orders.sql
โ””โ”€โ”€ marts/ โ† final tables consumed by BI tools and stakeholders
โ”œโ”€โ”€ dim_customers.sql
โ””โ”€โ”€ fct_orders.sql

This layering โ€” staging, intermediate, marts โ€” is the most common pattern on teams that use dbt seriously. It keeps individual models focused, limits blast radius when source schemas change, and makes lineage easy to follow.


What to Learn Next

You now have the foundation. The best way to learn the rest is to pick a real dataset and build models that answer questions you actually care about.