How to Use dbt: The Beginner's Guide (First Project, the ref() Rule, First Model, and the Test-vs-Docs Lesson)

CodexFuse Team · 2026-10-31T15:50:11.721Z · 15 min read

Learn how to use dbt: install dbt Core, scaffold your first project, write models with ref(), run tests, and publish the docs site — SQL only.

What is dbt?

Learning how to use dbt comes down to six moves: install dbt Core, run dbt init to scaffold a project, connect it to your warehouse in profiles.yml, write your first model as a plain SELECT, wire models together with ref(), and finish with dbt test and dbt docs. Everything else dbt offers — seeds, snapshots, incremental models — sits on top of that loop.

dbt (the data build tool) is an open-source tool that lets analysts and engineers transform data inside a data warehouse using only SQL. This dbt for beginners guide skips the cloud-first setup and runs everything locally, because nothing kills momentum like provisioning credentials before you have written a single model. If you want a zero-setup warehouse to practice against, our DuckDB beginner guide walks through an embedded analytical database that runs entirely on your laptop — it pairs naturally with the local dbt project below. Later, when you want to audit an existing model's SQL before rewriting it, the dbt model analyzer maps its refs, sources, CTEs, tests, and materialization directly in your browser.

Should you start with dbt Core or dbt Cloud?

Start with dbt Core. Core is the free, Apache-2.0-licensed command-line tool from dbt Labs — it does everything a beginner needs: models, tests, docs, seeds, and snapshots. What Core does not include is a job scheduler, a browser-based developer interface beyond the CLI, or CI/CD pipelines; those are what you (or a separate orchestrator) add later, and knowing that gap exists is part of the beginner lesson.

dbt Cloud is the managed SaaS platform built on top of dbt Core. It adds a browser-based IDE, a built-in job scheduler, native CI/CD integration, a semantic layer, and enterprise governance features such as SSO and RBAC. Both run the same transformation engine, so anything you learn in Core transfers directly if you adopt Cloud later.

Dimensiondbt Coredbt Cloud
CostFree, Apache 2.0Managed SaaS (a free Developer tier exists for a single user)
InterfaceCommand lineBrowser-based IDE plus CLI
SchedulingNone built in — use cron, Airflow, Prefect, or DagsterBuilt-in job scheduler
CI/CDWire up yourself (GitHub Actions, for example)Native integration
Best first stepLocal projects and learningTeams with production jobs

How do you install dbt?

Install dbt with pip or with uv, depending on how you manage Python on your machine. The official quickstart installs the mainline CLI with python -m pip install dbt (which lands you on the dbt 2.x line), while the long-standing open-source route is the dbt postgres adapter, installed with pip install dbt-postgres for a PostgreSQL-backed project. Never install dbt and the older dbt-oss package into the same Python environment — the docs warn they conflict.

Across 2026 tutorials, the community-recommended path is uv: uv tool install dbt-postgres keeps dbt in its own isolated environment, and uv run executes project commands without touching your system Python. Both routes work; pick one and stay consistent inside the project.

For a true zero-setup first run, install the DuckDB adapter instead: pip3 install dbt-duckdb. It targets dbt-core 1.8 and newer, and a DuckDB project needs no warehouse account, no hostname, and no credentials at all — dbt writes the database file on your own disk. That is the adapter this guide's beginner arc uses, because it removes every blocker between you and your first dbt build.

What does dbt init actually build?

dbt init scaffolds a working project in seconds — you answer a few prompts (project name, then the warehouse connection) and dbt generates the standard layout. Understanding the folder tree early is what separates a dbt tutorial you finish from one you forget, because every file you touch for the next year lives in one of these folders.

A fresh project contains: dbt_project.yml (project config — name, version, model defaults), models/ (your SQL models, with an example folder), macros/ (reusable Jinja/SQL blocks), tests/ (custom SQL tests), snapshots/ (snapshot definitions), seeds/ (static CSV files), analyses/ (ad-hoc queries that dbt compiles but never runs), plus .gitignore and a README. dbt also creates or updates ~/.dbt/profiles.yml — the connection file — based on your answers.

On the current mainline CLI, dbt init goes one step further: it generates the jaffle_shop example project, walks you through the connection profile, and automatically runs dbt debug at the end to verify the connection works. If you already have a profile and want to skip the prompts, dbt init --skip-profile-setup is the escape hatch.

How do you connect dbt to your warehouse?

dbt connects to your warehouse through ~/.dbt/profiles.yml, a YAML file that holds one profile per project and one target per environment. Keeping connections in profiles.yml — separate from dbt_project.yml — means your project code stays identical across dev, staging, and prod while only the target changes.

A PostgreSQL target looks like this:

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

Two details in that file are the beginner lessons in miniature. First, never hard-code a real password in profiles.yml — read it from an environment variable with env_var() so credentials stay out of version control. Second, the schema is where dbt builds your models; point dev at a personal schema so your experiments never collide with a teammate's.

The DuckDB target for the local arc is shorter, because there is nothing to authenticate:

jaffle_shop:
  outputs:
    dev:
      type: duckdb
      path: /tmp/jaffle_shop.duckdb
  target: dev

With only type: duckdb, dbt uses an in-memory database; add path: and dbt persists it to a local file, creating the file automatically if it does not exist.

What is a dbt model?

A dbt model is a single SQL file — usually one SELECT statement — that defines one table or view in your warehouse. This "everything is a SELECT" rule is the whole mental model of a dbt tutorial for beginners: write the query that produces the answer you want, save it as models/orders_summary.sql, and dbt handles creating the object, naming it, and re-running it when inputs change.

-- models/orders_summary.sql
select
    order_id,
    customer_id,
    order_date,
    amount
from {{ source('raw', 'orders') }}
where order_date >= '2026-01-01'

That one file already teaches the most important structural habit in analytics engineering: layer your models. The widely taught convention is three layers — staging (clean and rename raw tables, one model per source), intermediate (join and reshape), and marts (the final tables your dashboards read). dbt Labs' own materialization best-practice guide configures jaffle_shop exactly this way: staging models as views, intermediate models as ephemeral, marts as tables.

What are sources in dbt?

dbt sources are how you tell dbt which raw tables already exist in your warehouse, so dbt can track and document them. You declare them once in a YAML file, then reference them in models with the source() function — and by convention, source() calls appear only in your staging models, the first layer of the project.

# models/staging/_sources.yml
version: 2

sources:
  - name: raw
    database: analytics
    schema: raw
    tables:
      - name: orders
      - name: customers

Once declared, {{ source('raw', 'orders') }} compiles to the full analytics.raw.orders reference. Keeping raw-table references inside source() calls — and inside staging models only — is a widely taught best practice, not a compiler rule: it gives you one place to audit every external dependency your project has.

How does ref() connect your models?

The dbt ref() function is what turns a pile of SQL files into a pipeline. {{ ref('orders_summary') }} points at another dbt model, and dbt reads every ref() call to build the dependency DAG — the directed acyclic graph that decides the order models run in.

This is why dbt models stay small. Instead of one giant query, you write many short models and let ref() chain them: a staging model cleans the raw table, an intermediate model joins two staging models, and a mart model aggregates the intermediate result. Each ref() compiles to the real warehouse object name, including the right schema for your current target, so the same code works in dev and prod.

The ref() rule to internalize: never write a raw table name in a non-staging model, and never hard-code another model's schema. If you catch yourself typing a literal schema prefix, you meant to write a ref().

How do you run your project: dbt run or dbt build?

dbt run compiles your models and executes them in the warehouse in DAG order — it is the command you reach for while developing. dbt run --select orders_summary runs just one model and its ancestors, which keeps iteration fast while you are shaping a new transformation.

dbt build goes further: it runs your seeds, snapshots, models, and tests together, all in DAG order, in a single invocation. The beginner-friendly rhythm is dbt run while you write, dbt build when you want the whole project verified. After either command, dbt test re-runs just the tests, and dbt docs generate followed by dbt docs serve publishes the project's documentation website — the lineage graph, model descriptions, column definitions, and test results — to your browser.

What are seeds in dbt?

Seeds are CSV files in your project's seeds/ directory that dbt loads into your warehouse with dbt seed. They exist for small, static reference data that changes rarely — country-code mappings, excluded test email addresses, employee account IDs. Downstream models reference them exactly like models: {{ ref('country_codes') }}.

The beginner mistake is using seeds for real data loading. Seeds are version-controlled with your project, so they are the wrong tool for large tables, frequently updated exports, or anything containing PII or sensitive data — those belong in your warehouse's normal load process, declared as sources instead.

What are snapshots in dbt?

dbt snapshots implement type-2 slowly changing dimensions over mutable source tables: they record how a record's values change over time instead of just its current state. Define one in snapshots/, and dbt snapshot writes history rows with dbt_valid_from, dbt_valid_to, and dbt_scd_id columns marking when each version of the record was true.

Choose a strategy per snapshot: timestamp watches an updated-at column to detect changes, while check compares the actual column values you list. Run snapshots on a schedule — nightly is typical — and note that they deliberately ignore --full-refresh, so a full project rebuild never wipes your carefully collected history.

How do you test a dbt project?

dbt has four built-in generic tests: unique, not_null, accepted_values, and relationships. You attach them to a model or column in a schema YAML file, and they verify your data every time you run dbt test.

# models/marts/_schema.yml
version: 2

models:
  - name: orders_summary
    columns:
      - name: order_id
        tests:
          - unique
          - not_null
      - name: status
        tests:
          - accepted_values:
              values: ['placed', 'shipped', 'returned']

When you need logic the generics cannot express, write a singular test: a SQL file in tests/ whose query returns the offending rows — zero rows means the test passes. And since dbt 1.8, unit tests let you feed mock input rows to a model in YAML and assert its outputs without touching warehouse data, which makes transformation logic testable in CI before it ever runs against real tables.

How do you publish the docs site?

Every description you write in your schema YAML files becomes a living documentation website. dbt docs generate builds the site's metadata from your project, and dbt docs serve opens it locally — the full lineage graph showing which models feed which, every column description, every test, and every source declaration, all browsable in one place.

This is the test-vs-docs lesson the title promised: tests prove your data is right today; the docs site proves your project is understandable tomorrow. Beginners who write descriptions from day one end up with a project their whole team can navigate; beginners who skip them end up with a pile of models only they can read. If the docs site is the warehouse-facing artifact your stakeholders open, the downstream read is dashboards — our Metabase beginner guide covers the BI layer that queries the marts you just built.

What do the five dbt materializations mean?

Materialization decides how dbt physically creates each model in the warehouse. The default is view — dbt stores only the SQL logic and rebuilds it on every query, which is why iterating on a new model feels instant.

MaterializationWhat dbt createsUse it when
viewA view that re-runs the SQL on every queryDeveloping models, or when freshness matters more than speed
tableA real table rebuilt on each runThe model is queried often and should answer fast
incrementalA table built in pieces — only new or changed rows on each runLarge tables where rebuilding everything is wasteful
ephemeralNothing in the warehouse — interpolated as a CTE into downstream modelsIntermediate steps that only exist to feed other models
materialized_viewA database-native materialized viewYour warehouse supports it and you want managed refresh

Set materialization per model with a config block at the top of the SQL file, or for a whole folder in dbt_project.yml. Most beginners live happily with the defaults — views for staging, tables for marts — and reach for incremental only when a table grows large enough to make full rebuilds painful.

dbt vs Airflow: who schedules your runs?

dbt and Airflow answer different questions. dbt is the T in ELT: it transforms data that is already in the warehouse, tests it, and documents it. It deliberately does not decide when to run — dbt Core has no built-in scheduler and no cross-system orchestration.

That is Airflow's job. Airflow (or Kestra, Prefect, Dagster, or cron for simple cases) orchestrates your dbt runs: it decides the schedule, triggers dbt build, and handles retries and alerting when a run fails. The production pattern is simple — dbt transforms and validates, the orchestrator schedules. For the orchestrator branch of that pair, our Kestra beginner guide shows how to schedule dbt-style workflow runs declaratively.

7 beginner mistakes (and the fix for each)

MistakeWhy it hurtsThe fix
Printing real credentials into profiles.ymlThey end up in version control or screenshotsUse env_var() for passwords; keep profiles.yml local-only
Calling raw tables with source() deep in martsYou lose the one-place audit of external dependenciessource() only in staging models, ref() everywhere else
One giant model instead of layered modelsUntestable, unreusable, unexplainable SQLStaging → intermediate → marts, each model small
No tests on the first projectSilent data drift goes unnoticed for weeksAdd not_null + unique to every primary key from day one
Using seeds for large or changing dataBloated repo, slow clones, stale dataSeeds only for small static reference CSVs
Ignoring dbt debug failuresYou debug SQL that was never the problemRun dbt debug after every profile change before dbt run
Expecting dbt to schedule itselfYour "daily" pipeline never actually runs dailyPair dbt with an orchestrator for production runs

Your first 7 steps, in order

  1. Install dbt — pip3 install dbt-duckdb for the zero-setup local arc, or uv tool install dbt-postgres for PostgreSQL.
  2. Run dbt init and answer the prompts; let it write ~/.dbt/profiles.yml and run dbt debug to confirm the connection.
  3. Declare your raw tables in a _sources.yml under models/staging — one entry per source table.
  4. Write your first staging model — one SELECT per source table, cleaning column names and types, referencing tables only with source().
  5. Chain a mart model with ref() and watch dbt build the DAG; run it with dbt run --select <model>.
  6. Add tests — not_null and unique on the primary key in a schema YAML — then run dbt test.
  7. Publish the docs — dbt docs generate && dbt docs serve — and hand the lineage graph to your team.

Sources

Frequently asked questions

Is dbt free to use?

Yes — dbt Core is free and open source under Apache 2.0, and everything in this guide uses it. dbt Cloud is the paid managed platform, though it offers a free Developer tier for a single user.

Do I need a cloud warehouse to learn dbt?

No. Install the dbt-duckdb adapter and dbt runs against a local DuckDB file on your laptop — no warehouse account, no credentials, nothing to provision. That is the fastest way to practice models, tests, and the docs site.

What is the difference between dbt run and dbt build?

dbt run executes only your models in DAG order; dbt build runs your seeds, snapshots, models, and tests together in one invocation. Develop with run, verify with build.

Does dbt replace Airflow?

No. dbt is the transformation layer (the T in ELT); it has no built-in scheduler. Airflow or another orchestrator schedules your dbt runs and handles retries — they complement each other, and most production setups use both.

What should a beginner model against a real Postgres database?

Use the dbt-postgres adapter with a dev profile pointing at a personal schema, read the password from an environment variable with env_var(), and start with staging models that clean one source table each before you write any marts.

Continue your data stack

dbt is the transform layer of a modern data stack — it turns raw warehouse tables into tested, documented marts. If you want to inspect your dbt model files before they run, the dbt model analyzer breaks down pasted model SQL into refs, sources, CTEs, tests, and materialization right in your browser, no connection needed.

Featured on Twelve Tools Launch Llama Tools Launch your MCP