Module 2: Sources And Staging Models
Naming staging models: the `stg_` prefix
Description
Lesson 5 defined a staging model's shape — one source, no joins or aggregations. This lesson gives that shape two more things: a predictable name (stg_<entity>: stg_orders, stg_events, stg_stores, stg_products) and a fixed home inside the project (models/staging/kiosko/, grouped by source system). And it closes with the configuration piece that was missing: making it explicit, in dbt_project.yml, that everything living in that folder always materializes as view — not because it's the default inherited from module 1 (it is, for now), but as a declaration that survives even if that default changes later on.
Connection to the module. With the shape (lesson 5) and the name (this lesson) defined, lesson 7 finally writes the project's first two real .sql files: stg_orders.sql and stg_events.sql.
An analogy: the pantry label, not just the washed jar
Lesson 5 compared a staging model to an ingredient already washed and cut, ready in its own container. But an unlabeled container, in a pantry with twenty similar-looking containers, is almost as useless as not having it: any new cook has to open every one to know what's inside. A well-organized professional kitchen labels every mise en place container with a consistent prefix — something like PREP-tomato, PREP-onion — so that, just by glancing at the shelf, anyone can tell at a glance what's already prepped and what's still untouched raw material.
stg_ is that label. stg_orders tells anyone opening the project, with no need to read a single line of SQL: "this is orders, already clean, ready to be combined in a mart." And grouping all of them under models/staging/kiosko/ is, literally, the same shelf: all the prepped ingredients from the same supplier (Kiosko), together, separated from any other source system this project might have in the future.
Worked example: breaking down stg_orders
The name stg_orders has two parts, and both are fixed by convention, not by whim:
stg_— the prefix marking "this is a staging model." Always lowercase, always followed by an underscore. It is, precisely, the same convention documented by dbt Labs' official project structure guide (see Resources) — it's not this guide's invention.orders— the table's name inside the source, in plural, exactly as it's called in_sources.yml(source('kiosko_raw', 'orders')). It's never abbreviated, never translated, never given an extra qualifier — the one-to-one correspondence between the source table's name and the staging model's name is, itself, part of this layer's guarantee: anyone who seesstg_ordersknows, unambiguously, there's asource('kiosko_raw', 'orders')behind it.
This module's four names follow exactly that pattern:
| Source | Staging model | File |
|---|---|---|
source('kiosko_raw', 'orders') | stg_orders | models/staging/kiosko/stg_orders.sql |
source('kiosko_raw', 'events') | stg_events | models/staging/kiosko/stg_events.sql |
source('kiosko_raw', 'stores') | stg_stores | models/staging/kiosko/stg_stores.sql |
source('kiosko_raw', 'products') | stg_products | models/staging/kiosko/stg_products.sql |
Notice the folder: models/staging/kiosko/, not models/staging/ on its own. The second level — kiosko — groups by source system, not by table. In a real project with several source systems (for example, a CRM alongside the point-of-sale system), you'd see something like models/staging/kiosko/ next to models/staging/crm/ — each folder with its own staging models and its own _sources.yml. This guide only has one source system, but the folder structure is already ready for the day that changes, with no need to reorganize anything.
Worked example (continued): explicit materialization in dbt_project.yml
Since module 1, dbt_project.yml has declared models: kiosko_analytics: +materialized: view — the default materialization for the whole project. Since that default is already view, technically nothing more would need declaring for the staging models to be views. Even so, this guide adds an explicit configuration, nested under the staging folder:
# dbt_project.yml (add the "staging" block inside "kiosko_analytics")
models:
kiosko_analytics:
+materialized: view
staging:
+materialized: view
staging: here isn't a special dbt keyword — it's, literally, the name of the models/staging/ folder. dbt resolves each model's configuration by walking its folder path from outside in: first the entire project's default (kiosko_analytics:), then any specific configuration for a subfolder (staging:), and the more specific one always wins. Right now, both levels say view, so the result doesn't change — but if module 3 changed the whole project's default to table (something that, in fact, is going to happen once the marts start needing materialized tables), this explicit line still anchors the staging/ folder to view, with no need for you to remember to exclude it manually at that point.
You can check this yourself, without writing any real staging model yet. Create any temporary file inside models/staging/kiosko/:
-- models/staging/kiosko/_temp_check.sql (temporary, for this exercise)
select 1 as id
And check its resolved materialization, without running it, with dbt list:
dbt list --select _temp_check --output json --output-keys name resource_type config.materialized
What to expect.
{"name": "_temp_check", "resource_type": "model", "config.materialized": "view"}
Now, simulate what's going to happen in module 3: temporarily change the whole project's default to table, leaving the staging block untouched:
models:
kiosko_analytics:
+materialized: table # <- changed, simulating module 3
staging:
+materialized: view # <- unchanged
Run the same command again:
dbt list --select _temp_check --output json --output-keys name resource_type config.materialized
What to expect.
{"name": "_temp_check", "resource_type": "model", "config.materialized": "view"}
Exactly the same result: "view". Even though the whole project's default now says table, staging:'s nested configuration still wins for any model inside that folder — it's more specific, and dbt always resolves the more specific configuration over the more general one. Undo the change (set +materialized: view back at the project level, exactly as it was) and delete the temporary file:
rm models/staging/kiosko/_temp_check.sql
Going deeper: why view, and not table
A view (view) stores no data — it's, literally, a saved query DuckDB runs again every time someone queries it. A table (table) does physically store the result, once, at the moment you run dbt run. For a staging model, view is almost always the right choice, for three concrete reasons:
- It's cheap. A staging model does no
JOINorGROUP BY(lesson 5) — it's, at most, a column selection with a fewcast()s. Running that query again every time someone uses it costs almost nothing, very different from recomputing a heavyJOINacross several large tables. - It's always up to date. Since a view stores no physical copy, any query against
stg_ordersreflectsorders_*.csv's current state at that exact instant — there's no need to remember to "re-run" the view if the raw file changed. - It doesn't duplicate disk space. With four small source tables like Kiosko's this is practically irrelevant, but in a project with real data, materializing every staging model as
table— when none of them does heavy computation — wastes storage space with no speed gain to justify it.
Module 3 is going to show the opposite case: fact_orders, a mart that really does do a real JOIN across several tables, benefits from materializing as table — precisely because there the computation really is expensive, and it's worth paying for it once per run instead of on every query. The rule isn't "view is always better" — it's "the correct materialization depends on how much work the model does," and in staging, that work is almost nil by design.
Common mistakes
Naming a staging model in the singular, or with a suffix different from the source table. What happens: someone writes stg_order.sql (singular) or stg_orders_clean.sql (with an extra suffix), instead of stg_orders.sql. Why it happens: both variants feel reasonable in isolation — "a staging model describes one order at a time" or "clean makes explicit that it's already clean" — but they break the one-to-one correspondence between the source table's name and the staging model's name. How to spot it: always compare the name after stg_ against the exact name of the table inside source(...) — they should be identical, character for character. How to fix it: always use the same name as the source table, in the same form (singular or plural) that table's already called — orders in the source, stg_orders in the staging model, with no variation.
Putting staging models from different source systems in the same flat folder. What happens: in a project with more than one source system, someone puts every staging model straight into models/staging/, with no subfolders by system. Why it happens: with few models, a flat folder doesn't feel messy yet. How to spot it: if models/staging/ has loose .sql files, with no subfolders, and the project has (or is going to have) more than one source system, you already lost the grouping that makes it easy to spot "all of Kiosko's staging models" at a glance. How to fix it: always group by source system, as this guide does with models/staging/kiosko/ — even if today only one system exists, the structure is ready for when a second one shows up.
Confusing "it's in models/staging/" with "it's named stg_." What happens: someone puts a model that really does have a JOIN (breaking lesson 5's rule) inside the staging/ folder, but names it without the stg_ prefix, thinking that "excludes" it from the convention. Why it happens: it's easy to think the prefix and the location are separate things you can mix and match freely. How to spot it: location (models/staging/kiosko/) is what determines the materialization (view, from this lesson's nested config) — the file's name, on the other hand, is just a human readability convention. A file with no stg_ inside that folder still inherits view, even though the name doesn't announce it. How to fix it: the two rules — correct location and correct name — always go together, never one instead of the other; never use the staging/ folder for a model that doesn't also follow lesson 5's rule.
Exercises
Exercise 1 — Predict the correct name. If Kiosko added a fifth raw file called deliveries.csv (one row per order delivery), and you declared it as source('kiosko_raw', 'deliveries'), what would its staging model be called, and at what exact path would the file live?
See solution
stg_deliveries, in the file models/staging/kiosko/stg_deliveries.sql — the exact same pattern as this module's four staging models: stg_ prefix, name identical to the source's table, grouped under the source system's folder (kiosko, because deliveries.csv would still be a file generated by the same system that generates orders, events, stores, and products).
Exercise 2 — Verify the materialization from memory, with no code run. Without running any command, answer: if dbt_project.yml had models: kiosko_analytics: +materialized: table (with no nested staging: block), what materialization would a new file at models/staging/kiosko/stg_deliveries.sql get?
See solution
table — with no nested staging: block to override it, any model inherits the default from the most specific configuration level that exists for its path; if the only declared level is the whole project's (kiosko_analytics:), that's what applies. This is exactly why this lesson adds the explicit staging: +materialized: view block — without it, a future change to the whole project's default (like the one that's going to happen in module 3) would drag the staging models along with it, unintentionally.
Exercise 3 — Explain the analogy in your own words. Using this lesson's pantry-label comparison, explain in 2-3 sentences what concrete information the simple fact of seeing a file called stg_products.sql inside models/staging/kiosko/ gives a new teammate, without that person having read a single line of its content.
See solution
Just from the name and location, a new teammate already knows this model corresponds exactly to source('kiosko_raw', 'products') (from the name after stg_), that it belongs to Kiosko's source system (from the folder), that it materializes as a view that's cheap to recompute (from being inside staging/), and that, by lesson 5's rule, it doesn't combine products with any other table — all of that information travels in the file's name and location, with no need to open it, exactly the way a well-written pantry-jar label saves you from having to uncap it to know what's inside.
Summary and next step
In this lesson you completed the staging layer's definition: the stg_ prefix followed by the exact name of the source table, grouped by source system under models/staging/kiosko/, and a view materialization explicitly declared in dbt_project.yml — nested under the staging folder — so it survives even when the whole project's default changes in module 3. You confirmed, with dbt list --output json, that nested configuration wins over the general default, even when both point to different places.
Before moving on you should be able to: name from memory this module's four staging models and their exact paths; and explain, with no code run, why view is the right materialization for this specific layer.
With the shape, the name, and the configuration already resolved, lesson 7 finally writes the project's first two real .sql files: stg_orders.sql and stg_events.sql.
Resources
- dbt Developer Hub — "How we structure our dbt projects: staging," the official source for the
stg_<entity>convention and grouping by source system. docs.getdbt.com/best-practices/how-we-structure/2-staging. In English. - dbt Developer Hub — "About materializations," the official reference for
viewversustableand the other materializations you're going to meet in module 3. docs.getdbt.com/docs/build/materializations. In English. - dbt Developer Hub — "Model configurations," how dbt resolves nested configuration by folder — the technical basis for this lesson's worked example. docs.getdbt.com/reference/model-configs. In English.