Data Modeling and Transformation with dbt
This domain tests your ability to structure transformations, choose materializations, and express dependencies correctly. The exam favors practical decisions: which layer a model belongs in, when an incremental model is safe, and how configuration precedence changes the compiled result.
Layering and references
A reliable dbt project separates staging, intermediate, and mart models. Staging models map one-to-one with sources, rename and cast columns, and avoid business joins. Intermediate models hold reusable logic; marts expose business concepts. Use source() for raw tables and ref() for models. ref() both resolves the relation and creates the lineage edge that deployment, documentation, and selection depend on.
Materializations and incremental logic
Views are the default and cost little to build but recompute on every query. Tables rebuild fully. Incremental models process a filtered subset and apply a strategy such as merge, delete+insert, or insert_overwrite, depending on the adapter. A unique_key makes merge updates possible; without one, append-style strategies can duplicate rows. Wrap the incremental filter in {% if is_incremental() %} so the first build still loads history. on_schema_change controls whether new columns are ignored, appended, or synced.
Configuration precedence
Configurations can be set in dbt_project.yml, in a model's config() block, or on the command line. More specific scopes win. Contracts, when enforced, require declared columns and data types and make the warehouse reject incompatible changes.
Worked example
An events model has event_id as a stable key and late-arriving records. Set materialized='incremental', unique_key='event_id', and on_schema_change='append_new_columns'. Inside the model, select only rows newer than max(event_at) from the existing relation when is_incremental() is true. The first run takes the full select because the relation does not exist; later runs merge new and corrected events by key. A --full-refresh rebuilds the whole table and is the recovery path after logic changes.
Traps that cost marks
Using source() where ref() is required breaks lineage and environment switching. Putting an incremental filter outside is_incremental() silently drops historical rows on the first build. Choosing a view for a large fact table pushes cost to every consumer. Ephemeral models are inlined as CTEs and are not queryable objects, so they are a poor choice for reused marts. Folder-level configs can be overridden unintentionally by model-level configs.