Back to blogLearn

The Complete Guide to dbt™ Schema Configuration

Everything you need to know about dbt™ schemas: how they work, why concatenation happens, and how to configure them properly for dev and prod.

Fabio Di Leta
Fabio Di LetaNov 20, 20255 min read
The Complete Guide to dbt™ Schema Configuration

dbt™ schema configuration is straightforward once you understand the mental model. This guide covers everything: how schemas work, where models land, why concatenation happens, and how to control it all.

How dbt™ Schema Configuration Works

Start in profiles.yml. This is where you define your target schemas:

my_project:
  target: dev
  outputs:
    dev:
      type: snowflake
      database: analytics_dev
      schema: dbt_john
      # ... connection details

    prod:
      type: snowflake
      database: analytics_prod
      schema: analytics

Run dbt run --target dev and every model lands in analytics_dev.dbt_john by default.

analytics_dev.dbt_john.my_model

That's your base schema. Everything else builds from here.

Target Schema vs Custom Schema

Two different things. Here's what they actually are.

Target schema: Where dbt™ puts your models by default. Set in profiles.yml:

my_project:
  target: dev
  outputs:
    dev:
      schema: dbt_john  # ← This is your target schema

Run dbt run --target dev and every model lands in dbt_john. That's it.

Custom schema: Your override. Set in the model or in other parts of your dbt™ project:

{{ config(schema='staging') }}  # ← This is your custom schema

select * from {{ source('f1', 'circuits') }}

When you set a custom schema, dbt™ doesn't just use it. It concatenates it with your target schema.

Example:

  • Target schema: dbt_john
  • Custom schema: staging
  • Where the model lands: dbt_john_staging

Not staging. dbt_john_staging.

Why? So five developers can work on the same project without destroying each other's work. You get dbt_john_staging, Jane gets dbt_jane_staging. Nobody collides.

In production, you want clean names. Override the macro to drop concatenation for prod targets only.

Understanding Schema Concatenation

Add a custom schema to your model:

{{ config(schema='staging') }}

select * from {{ source('f1', 'circuits') }}

You expect: analytics_dev.staging.my_model

You get: analytics_dev.dbt_john_staging.my_model

This is the generate_schema_name macro at work. Default behavior:

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- set default_schema = target.schema -%}
    {%- if custom_schema_name is none -%}
        {{ default_schema }}
    {%- else -%}
        {{ default_schema }}_{{ custom_schema_name | trim }}
    {%- endif -%}
{%- endmacro %}

That underscore in {{ default_schema }}_{{ custom_schema_name }} creates the concatenation.

Why? Developer isolation. Five people working on the same project need their own space. Jane gets dbt_jane_staging, you get dbt_john_staging. Nobody collides.

Controlling Schema Behavior: Development vs Production

Production needs clean schemas. staging, not analytics_staging. Override the macro.

Create macros/get_custom_schema.sql:

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- set default_schema = target.schema -%}

    {%- if target.name == 'prod' -%}
        {%- if custom_schema_name is none -%}
            {{ default_schema }}
        {%- else -%}
            {{ custom_schema_name | trim }}
        {%- endif -%}
    {%- else -%}
        {%- if custom_schema_name is none -%}
            {{ default_schema }}
        {%- else -%}
            {{ default_schema }}_{{ custom_schema_name | trim }}
        {%- endif -%}
    {%- endif -%}
{%- endmacro %}

Now:

  • Production: schema='staging' → staging
  • Dev: schema='staging' → dbt_john_staging

Done.

Schema Configuration Priority

Four places to set schemas. Most specific wins.

1. Model Config Block

{{ config(schema='staging') }}

Highest priority. Overrides everything.

2. dbt_project.yml

models:
  my_project:
    staging:
      +schema: staging
      +materialized: view

    marts:
      +schema: marts
      +materialized: table

      finance:
        +schema: finance

Directory-level configs. Applies to all models in the tree.

3. schema.yml

models:
  - name: stg_customers
    config:
      schema: staging

Model properties. Lower priority than config blocks.

4. Target Schema

From profiles.yml. Your base schema. Used when nothing else is set.

Common Schema Patterns

Layered Architecture

models:
  my_project:
    staging:
      +schema: staging

    intermediate:
      +schema: intermediate
      +materialized: ephemeral

    marts:
      +schema: marts

      core:
        +schema: core

      finance:
        +schema: finance

Multiple Production Targets

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- if target.name in ['prod', 'production'] -%}
        {{ custom_schema_name | trim }}
    {%- else -%}
        {{ target.schema }}_{{ custom_schema_name | trim }}
    {%- endif -%}
{%- endmacro %}

No Concatenation Ever

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- if custom_schema_name is none -%}
        {{ target.schema }}
    {%- else -%}
        {{ custom_schema_name | trim }}
    {%- endif -%}
{%- endmacro %}

Solo developer? Use this.

Debugging Schema Issues

See where models compile

dbt compile --select my_model
# Check target/compiled/my_project/...

Full schema.database.table reference is right there in the compiled SQL.

List models with locations

dbt ls --select my_model --output json

Debug the macro

{% macro generate_schema_name(custom_schema_name, node) -%}
    {{ log("Target: " ~ target.name, info=True) }}
    {{ log("Schema: " ~ target.schema, info=True) }}
    {{ log("Custom: " ~ custom_schema_name, info=True) }}

    -- your logic
{%- endmacro %}

Run dbt compile and watch what values get passed in.

Best Practices

Override generate_schema_name for production. The default concatenation is for dev isolation only.

Pick a schema naming pattern and stick with it. Staging/intermediate/marts or bronze/silver/gold - doesn't matter which. Consistency matters.

Set schemas at directory level in dbt_project.yml. Less config duplication.

Test in dev first. Run dbt compile and verify models land where you expect before deploying.

Document your pattern. Drop a note in the project README for new developers.

What Not to Do

Don't Remove Concatenation Everywhere

Some people hate the concatenation and do this:

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- set default_schema = target.schema -%}
    {%- if custom_schema_name is none -%}
        {{ default_schema }}
    {%- else -%}
        {{ custom_schema_name | trim }}  # ❌ Missing target.schema prefix
    {%- endif -%}
{%- endmacro %}

Works great in production. Catastrophic in shared dev/CI environments.

Here's what happens with a model called my_model and schema='marketing':

Environment

Target Schema

Result

Production

analytics

prod.marketing.my_model ✅

Developer - John

dbt_john

dev.marketing.my_model ❌

Developer - Jane

dbt_jane

dev.marketing.my_model ❌

CI PR 123

dbt_pr_123

ci.marketing.my_model ❌

CI PR 234

dbt_pr_234

ci.marketing.my_model ❌

See the problem? Everyone writes to the same schema. John and Jane overwrite each other's work. PR builds collide. Nobody knows whose version is currently deployed.

Fix: Only drop concatenation in production.

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- set default_schema = target.schema -%}

    {%- if target.name == 'prod' -%}
        {{ custom_schema_name | trim }}  # Clean names in prod
    {%- else -%}
        {{ default_schema }}_{{ custom_schema_name | trim }}  # Isolation everywhere else
    {%- endif -%}
{%- endmacro %}

Production gets clean schemas. Everyone else gets their own namespace. Problem solved.

Key Takeaways

  • Default behavior: dbt™ concatenates your target schema with custom schemas. That's for dev isolation.
  • Override generate_schema_name to control schema behavior by environment.
  • Production gets clean schema names. Dev gets concatenated names. Everyone stays in their lane.
  • Configure at model, directory, or project level. Most specific wins.
  • Test with dbt compile before deploying.

Interested to Learn More? Try Out the Free 14-Days Trial

One AI-native workspace to build, ship, and monitor dbt™ — without the tool sprawl.

More Articles

Join industry leaders winning with DinoAI agents

Data engineers and analysts at the most AI-native startups and enterprises use Paradime's DinoAI agents everyday to become market leaders.