Module 3: Ref Marts And Materializations

Module overview: from staging views to the star schema with `ref()`

Description

Module 2 closed with four clean views — stg_orders, stg_events, stg_stores, stg_products — each reflecting, with no loss or duplication, exactly one Kiosko raw file. They're a reliable starting point, but they still aren't a dimensional warehouse: no one can answer "how much did each store sell this week?" by querying four standalone views that never combine with each other. That, precisely, is this module's work.

This module rebuilds, inside kiosko_analytics/, the three central pieces of the star schema data-modeling-for-analytics-guide already hand-designed: the store dimension (dim_store), the date dimension (dim_date), and the sales fact (fact_orders). The difference from that earlier guide isn't in the model — the grain, the columns, and the star schema's shape were already decided and aren't justified again here — it's in how each piece connects to the next. You're going to meet ref(), the function that replaces "I remember dim_store has to exist before fact_orders" with an explicit declaration dbt reads, validates, and resolves for you. And you're going to learn to choose, with judgment and not out of habit, between the two materializations you've already used without thinking much about it: view (everything in staging, since module 2) and table — the one these three new marts are going to need, and that you've never configured on purpose until now.

Connection to the module. Everything you built in modules 1 and 2 — dbt_project.yml, profiles.yml, the four sources, the four staging views — stays exactly the same; none of that gets touched or re-explained. What changes is that, for the first time in this guide, a model is going to depend on another dbt model, not just on a raw file. That's this module's complete conceptual leap: from "a model reads a source" to "a model reads another model, which reads another model."

An analogy: the blueprint that says which floor gets built before which

Think about the difference between building a building following an engineering blueprint, and building it trusting the foreman to remember, from memory, that floor 3 can't be poured before floor 2 has cured. The blueprint doesn't just describe each floor separately — it also declares, explicitly, its dependencies: "floor 3 doesn't start until floor 2 is finished." Any new crew can read that blueprint and know, without asking anyone, what order to work in. And if someone tried to skip a step — pouring floor 3 before floor 2 even exists — the blueprint itself makes that mistake impossible to overlook: there's nowhere to rest the formwork.

ref() is that dependency declaration between floors, applied to dbt models. When fact_orders.sql writes {{ ref('dim_store') }} instead of the plain table name dim_store, it isn't just saying "read from here" — it's saying, in a way dbt can read and verify, "this floor doesn't get built before that other one." From that declaration, dbt assembles the building's complete blueprint — the DAG, directed acyclic graph, a dependency graph with no cycles — and decides, on its own, what order to pour each floor in. This module's lesson 2 shows you that mechanism working, with real SQL and real errors, the same way module 2's lesson 3 showed you source().

This module's map

Lesson    Question it answers
────────  ──────────────────────────────────────────────────────────
L1        (this one) What this module is about, and why ref() matters
L2        What the ref() function does, exactly -- and the DAG it builds
L3        Materializations: view versus table, and what changes between them
L4        Rebuilding dim_store and dim_date as real dbt models
L5        Rebuilding fact_orders by joining three models via ref()
L6        How to choose the right materialization for each Kiosko model
L7        Reading the project's DAG with dbt ls, without running anything
L8        Project: Kiosko's complete star schema, inside dbt

The progression is the same one you already saw in module 2, applied one level up: lessons 2 and 3 resolve the two new mechanisms (ref(), materializations) in isolation, with small, deliberately trivial examples. Lessons 4 and 5 are where those mechanisms build something real: Kiosko's three marts, in the order a star schema actually gets assembled — dimensions first, then the fact that references them. Lesson 6 comes back to materializations, this time with decision criteria applied to each concrete model in the project. Lesson 7 teaches you to read the DAG ref() built, without running a single model. And lesson 8 closes out with the complete project, verified end to end.

What you're going to build, previewed

By the end of this module, models/marts/ is going to contain three new files:

ModelWhat it isMaterializationDepends on (ref())
dim_storeStore dimensiontablestg_stores
dim_dateDate dimension, all of August 2026table(none — generates its own range)
fact_ordersSales fact, order-line graintablestg_orders, dim_store, dim_date

Notice something before writing a single line of SQL: dim_date is the only one of the three that does not depend on any other dbt model — it generates its own fixed date range with pure SQL, without reading any source or any staging model. You're going to build it that way in lesson 4, and the exact reason — why a date dimension doesn't need a source table — is part of that lesson's content, not a spoiler that ruins the exercise.

By the end of the module, dbt run over the complete project is going to build seven models — module 2's four staging views, plus these three new marts — in the correct order, with you declaring that order nowhere else but inside each .sql file, with ref(). And fact_orders, this module's final table, is going to have exactly 40 rows — the same number you already verified in stg_orders in module 2, and the same one you verified by hand in data-engineering-foundations-guide — with a revenue sum of 106.15: the concrete proof that joining three models with ref() didn't lose or duplicate a single order.

The boundary: what this module does NOT explain again

Why fact_orders has that specific grain, and why the model is a star schema and not a snowflake, was already answered in data-modeling-for-analytics-guide. That guide walked through Kimball's complete process — declaring the grain, choosing the dimensions, deciding between star and snowflake — over this same Kiosko data, and arrived at the exact columns you're going to write in lesson 5 (order_id, store_id, product_id, quantity, unit_price, revenue, order_ts). This module does not justify those decisions again: it takes them as given, and focuses on a different, narrower question — how do you translate an already-designed dimensional model into dbt .sql files, chained with ref()? If at any point in this module you feel like an explanation of "why this column" or "why this grain" is missing, that answer lives in the earlier guide, not here.

Nor does any of the following belong in this module — it arrives in the following ones: tests on the new marts (module 4, with the data_tests: key you already know from module 2), snapshots to historize dim_product (module 5), incremental models so fact_orders doesn't get rebuilt whole on every run (module 6), or macros and generated documentation (module 7). This module builds the marts with the correct shape and the correct materialization — reliable by design, but still without an automated test net woven around them.

Common mistakes

Expecting this module to re-explain why fact_orders has those columns and not others. What happens: someone who didn't go through data-modeling-for-analytics-guide reaches lesson 5 expecting a design argument — "why is the grain an order line and not a whole order?" Why it happens: it's natural to assume any guide that builds a dimensional model also justifies it from scratch. How to spot it: if in lesson 5 you look for an explanation of why those seven columns are the right ones, and only find how to write them as dbt SQL, that's the sign you're in the right place for the wrong question. How to fix it: if you need that design argument, now's the time to read (or review) data-modeling-for-analytics-guide — this guide assumes that decision is already made and focuses on the implementation.

Confusing ref() with a plain text substitution for the table name. What happens: someone thinks {{ ref('dim_store') }} is "a longer way of writing dim_store," with no real gain over writing the plain name. Why it happens: in a project that's already built and run, both forms compile to the same final SQL, so the difference doesn't jump out with just one model. How to spot it: lesson 2 shows you the exact case where the difference stops being cosmetic — an empty database, a model that depends on another one that doesn't exist yet. How to fix it: read all of lesson 2 before writing your own ref(); the difference between "substituting text" and "declaring a dependency dbt resolves" is, precisely, that lesson's central topic.

Exercises

Exercise 1 — Predict the build order. Without having read lesson 2 yet, and using only this lesson's "What you're going to build" table, predict: if you asked dbt to build the project's seven models (the four staging ones plus the three new marts) in a completely empty database, which model(s) would have to finish building before fact_orders could even start?

See solution

According to the table, fact_orders depends on stg_orders, dim_store, and dim_date. Of those three, dim_store in turn depends on stg_stores — so the complete chain that has to finish before fact_orders is actually: stg_orders, stg_stores, dim_store, and dim_date (which doesn't depend on anything else). The exact order among those four doesn't matter as long as each one respects its own dependencies — stg_stores before dim_store, for example — but all four have to exist before fact_orders can run its first SELECT.

Exercise 2 — Find the model with no dbt dependencies. Of this module's three new models, one doesn't depend on any other dbt model via ref(). Without reading lesson 4 yet, which one is it, and why do you think a date dimension wouldn't need to read any real Kiosko data to exist?

See solution

It's dim_date. Unlike dim_store (which needs the three stores' real data) or fact_orders (which needs the real orders), a typical date dimension doesn't describe any specific Kiosko data — it describes the calendar itself: what day of the week August 5, 2026 falls on, which quarter it's in, whether it's a weekend. That information can be generated in full with pure SQL, without reading any external file, for whatever date range the business needs to cover — lesson 4 shows exactly how.

Exercise 3 — Argue the boundary in your own words. In 2-3 sentences, explain why it makes sense for this module not to justify fact_orders's grain again, using this lesson's building-blueprint analogy.

See solution

A building's blueprint doesn't re-justify, on every floor, why the building has that many floors or that room layout — that design decision was already made at an earlier stage (the architect), and the construction blueprint focuses on the order and technique of execution, not on the original "why." In the same way, data-modeling-for-analytics-guide already decided, through Kimball's complete process, that fact_orders has order-line grain — this module receives that decision already made and focuses on a different question: how to declare, with ref(), that this model depends on those others, and with what materialization to build it.

Summary and next step

This module closes the distance between module 2's four staging views and a real star schema, chained with ref() and materialized with judgment. You saw the map of the 8 lessons, the complete table of what you're going to build — dim_store, dim_date, fact_orders, with their dependencies and materializations already previewed — and this module's hard boundary: the dimensional model's grain and shape were already decided in data-modeling-for-analytics-guide, and here they get translated to dbt without being justified again.

Before moving on you should be able to: name the three marts you're going to build and explain, in one sentence, what depends on what; and explain in your own words the difference between "writing a table's name" and "declaring a dependency with ref()," even though you haven't seen the mechanism in detail yet.

Lesson 2 is where ref() stops being a promise and becomes real code, run on purpose to fail in several different ways — the same treatment module 2's lesson 3 gave source().

Resources

  • dbt Developer Hub — "ref()," the official reference for this module's central function, the basis for lesson 2. docs.getdbt.com/reference/dbt-jinja-functions/ref. In English.
  • dbt Developer Hub — "About materializations," the official reference for view, table, and the other materializations, the basis for lesson 3. docs.getdbt.com/docs/build/materializations. In English.
  • dbt Developer Hub — "How we structure our dbt projects: marts," dbt Labs's official convention for the models/marts/ folder this module starts populating. docs.getdbt.com/best-practices/how-we-structure/4-marts. In English.
  • data-modeling-for-analytics-guide — the sibling guide that hand-designed, with Kimball's complete process, the same star schema this module rebuilds as a dbt project. Internal document of the data-engineering-ecosystem ecosystem.