Home / Data Platform / dbt & Quality

Data PlatformHow-toBeginner

dbt basics: your first project with sources, models, ref, tests and docs

Set up a dbt Core project, declare sources, build staging and mart models with ref(), add built-in tests, choose materializations, and generate documentation, with the commands you will run every day.

Deepak Bhardwaj3 min read

Applies todbt Core 1.7+ and dbt Cloud, adapters for Snowflake, Databricks, BigQuery, Postgres and others

dbt turns SQL SELECT statements into tables and views in your warehouse, in the right order, with tests. You write models/*.sql; dbt works out dependencies and runs them.

Install and create a project

python -m venv .venv && source .venv/bin/activate
pip install dbt-core dbt-postgres          # or dbt-snowflake, dbt-databricks, ...
dbt init shop_analytics
cd shop_analytics
dbt debug                                  # checks profiles.yml and the connection

Connection details live in ~/.dbt/profiles.yml, not in the project, so credentials stay out of Git.

shop_analytics:
  target: dev
  outputs:
    dev:
      type: postgres
      host: localhost
      user: dbt_dev
      password: "{{ env_var('DBT_PASSWORD') }}"
      port: 5432
      dbname: analytics
      schema: dbt_alice
      threads: 4

Each developer gets their own schema, so nobody overwrites anyone else's work.

Declare sources

models/staging/sources.yml:

version: 2
sources:
  - name: shop
    schema: raw_shop
    tables:
      - name: orders
      - name: customers

Sources are the raw tables dbt reads but does not build.

Staging models

models/staging/stg_orders.sql:

select
    id            as order_id,
    customer_id,
    cast(amount as numeric(10,2)) as amount,
    cast(created_at as date)      as order_date,
    status
from {{ source('shop', 'orders') }}
where status != 'test'

One staging model per source table: rename, cast, filter junk. Nothing else.

A mart model with ref()

models/marts/fct_daily_revenue.sql:

select
    o.order_date,
    c.country,
    count(*)        as orders,
    sum(o.amount)   as revenue
from {{ ref('stg_orders') }} o
join {{ ref('stg_customers') }} c using (customer_id)
group by 1, 2

ref() is the heart of dbt. It points at another model and tells dbt to build that model first. Never hard-code schema names in models.

Build and run

dbt run                                   # build all models
dbt run --select fct_daily_revenue        # one model
dbt run --select +fct_daily_revenue       # it and everything upstream
dbt build                                 # run + test + snapshots + seeds, in order

Tests

models/staging/schema.yml:

version: 2
models:
  - name: stg_orders
    columns:
      - name: order_id
        tests: [unique, not_null]
      - name: status
        tests:
          - accepted_values:
              values: ['placed', 'shipped', 'returned', 'cancelled']
      - name: customer_id
        tests:
          - relationships:
              to: ref('stg_customers')
              field: customer_id
dbt test
dbt test --select stg_orders

A failing test prints the SQL it ran; paste it into your SQL client to see the bad rows.

Tip

unique and not_null on every primary key catch most join explosions and duplicate loads before they reach a dashboard.

Materializations

Materialization What dbt creates Use for
view (default) A view Light staging models
table A table rebuilt each run Marts read often
incremental A table where only new rows are processed Large fact tables
ephemeral Nothing; inlined as a CTE Small helper logic

Set it per folder in dbt_project.yml:

models:
  shop_analytics:
    staging:
      +materialized: view
    marts:
      +materialized: table

Documentation

dbt docs generate
dbt docs serve --port 8081

You get a site with every model, column description and a lineage graph. Add descriptions in the YAML files as you go.

Daily commands

Command When
dbt build --select state:modified+ In CI, build only what changed (needs a saved manifest)
dbt compile --select model See the SQL dbt will run
dbt ls --select +model List what a selection covers
dbt run --full-refresh --select model Rebuild an incremental model from scratch (careful in prod)

Prevent it next time

  • Every model goes through ref() or source(); no hard-coded table names.
  • Tests on keys for every model, run in CI before merge.
  • Separate dev schemas per developer and a prod target only the scheduler uses.