A dbt materialization is the execution strategy that decides how a model is built. In addition to built-in choices like table, view, and incremental, you can implement a custom materialization tailored to your requirements.
This article walks through design principles for safety and portability that follow the official documentation, a minimal implementation, and operational gotchas — along with points that are likely to show up on the Analytics Engineer exam.
A materialization is defined as a special Jinja block and selected from the model side via config(materialized='name'). dbt compiles the model SQL, then runs the materialization block with that SQL and the target relation (this) as inputs.
The safe baseline pattern is: build into a temp table → swap via rename → return the artifact. This avoids exposing half-finished state.
Execution flow (from compile to swap)
The Analytics Engineer exam tends to ask about the differences between built-in and custom materializations, when to use which, and how dependencies and grants are handled. When designing, prioritize portability and observability on failure.
In particular, do not mix processing that belongs in on-run-end hooks or post-hooks inside the materialization body, and keep grant application clearly separated. This separation of concerns matters.
| Type | Update Mode | Typical Use Cases | Watch Out For |
|---|---|---|---|
| view (built-in) | Recomputed | Lightweight aggregation and exploration | Grants depend on the underlying tables |
| table (built-in) | Rebuilt | Stable delivery and cases with many downstream dependencies | Build time and storage cost grow |
| incremental (built-in) | Delta / merge | Event-driven or daily-partitioned data | Key management and a recomputation strategy are required |
| custom | Arbitrary (swap / split / combined) | Optimized for business requirements | Testing and rollback design are key |
The most general-purpose and safe custom approach writes the compiled SQL to a temp table, then renames it onto the production relation once it is complete. It uses helper macros and the adapter API that work across most adapters.
Below is a minimal implementation, along with an example of how to opt in from the model.
macros/materializations/swap_table.sql and an example model
-- macros/materializations/swap_table.sql
{% materialization swap_table, adapter='default' %}
{% set target = this %}
{% set tmp = make_temp_relation(target) %}
{# Drop any pre-existing temp table #}
{% do adapter.drop_relation(tmp) %}
{# Create into the temp table (compiled SQL = sql) #}
{% call statement('create_tmp', fetch_result=False) %}
{{ create_table_as(tmp, sql) }}
{% endcall %}
{# Drop the old table and swap #}
{% do adapter.drop_relation(target) %}
{% do adapter.rename_relation(tmp, target) %}
{# Return the artifact #}
{{ return({'relations': [target]}) }}
{% endmaterialization %}
-- models/fct_orders.sql
{{ config(materialized='swap_table') }}
select *
from {{ ref('stg_orders') }}
where order_status != 'CANCELLED';Operationally, hooks, grant application, and cleanup-on-failure design matter. Side-effect work such as ANALYZE/OPTIMIZE in post-hooks should be kept separate from the materialization body so that re-runs are safer.
Define grants in the model config, and where possible call apply_grants at the end of the materialization. For adapters that do not support it, fall back to a post-hook.
When you want warehouse-specific optimizations, keep the default implementation and override only the adapters that need it. Materialization blocks branch via the adapter argument.
In this shape, unsupported adapters inherit the default while supported adapters can override with minimal diffs.
Adapter-specific override example (conceptual)
{% materialization swap_table, adapter='default' %}
{# Shared swap implementation #}
...
{% endmaterialization %}
{% materialization swap_table, adapter='snowflake' %}
{# Only describe the diff — for example, add CLUSTER BY for Snowflake #}
...
{% endmaterialization %}Verify a new materialization on a small model, and confirm behavior under both dbt run -m model_name --full-refresh and a normal run.
When trouble strikes, inspect the generated SQL, the statement logs, and any leftover tmp relations. A missing relations array in the return value affects the catalog and dependency graph.
Analytics Engineer
問題 1
You implemented a custom dbt materialization. After setting config(materialized='swap_table') on the model and running it, the artifact is created, but the relation is not recognized by the documentation or dependency resolution. Which is the most likely cause?
正解: A
A custom materialization must return a list of relations once it finishes. Without return({'relations': [target]}), the artifact does not propagate to the catalog or the dependency graph. The presence or absence of is_incremental() and post-hooks is not the direct cause. Setting adapter to something other than default still works, as long as it matches the active adapter.
When should I replace incremental with a custom materialization?
When the standard incremental delta/merge behavior is not enough and you need to atomically express specific business logic — period-based swaps, commit-after-two-phase validation, simultaneously swapping multiple tables, and so on. Always check first whether the built-in materializations can do the job.
Should grants be applied inside the materialization or in a post-hook?
For re-runnability and separation of concerns, either call apply_grants with the model's grants config at the end of the materialization, or move grants to a post-hook. Pick one based on your environment and adapter support, and stick with it consistently.
What do I do when renaming a temp table fails (due to locks or permissions)?
First check DROP/RENAME privileges and schedule jobs to avoid contention. If that is unavoidable, branch within statement() to fall back to a CREATE OR REPLACE strategy.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
dbt Models: SQL-Defined Transformation Units (2026)
Model fundamentals — SELECT-based definitions, naming, refs,...
dbt Analytics Engineering Exam: Complete Guide (2026)
Pass the AE Certification — scope, weighting, sample questio...
dbt Cloud vs dbt Core: Feature & Cost Comparison (2026)
Honest comparison of dbt Cloud vs. dbt Core — IDE, scheduler...
dbt Project Structure: models/seeds/macros Layout (2026)
Recommended dbt project layout — models, seeds, macros, snap...
dbt_project.yml Explained: Every Config (2026)
Every dbt_project.yml setting that matters — paths, vars, ma...