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.


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: analyticsRun dbt run --target dev and every model lands in analytics_dev.dbt_john by default.
analytics_dev.dbt_john.my_modelThat'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 schemaRun 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: financeDirectory-level configs. Applies to all models in the tree.
3. schema.yml
models:
- name: stg_customers
config:
schema: stagingModel 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: financeMultiple 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 jsonDebug 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 |
|
|
Developer - John |
|
|
Developer - Jane |
|
|
CI PR 123 |
|
|
CI PR 234 |
|
|
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_nameto 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 compilebefore 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.


