Why Staging Models Are More Than SQL: The Hidden Power of dbt Tests and Data Contracts
From Raw Data to Business Data Products (Part 4)
After loading the Airbnb dataset into Snowflake, I moved to the next layer of the architecture: dbt staging models.
At first, I thought staging was just about cleaning data:
- Renaming columns
- Fixing data types
- Selecting fields
But as I started building more models, I discovered something I initially underestimated:
Staging models are not just transformation logic, they are also the first layer of data quality enforcement.
And in dbt, that enforcement happens through something most beginners ignore:
YAML-based testing and documentation.
The Missing Piece in My First Approach
My early staging models looked like this:
SELECT
id AS listing_id,
host_id,
neighbourhood AS neighborhood,
room_type,
price,
minimum_nights,
review_scores_rating
FROM {{ source('raw', 'listings') }}
This handled structure.
But it did NOT answer important questions like:
- Is
listing_idactually unique? - Can it ever be null?
- Do reviews always belong to a valid listing?
- Can calendar data have duplicate date rows?
Without answers to these questions, the models looked correct, but weren't trustworthy.
That's when I realized:
Transformations without validation are incomplete.
Introducing dbt YAML: Turning Models Into Contracts
In dbt, every model can be paired with a YAML file that defines:
- What the model represents
- What each column means
- What constraints it must satisfy
This is where analytics engineering starts to feel closer to software engineering.
Instead of only writing transformations, we also define expectations.
Here is the staging layer configuration I used:
models:
- name: stg_listings
description: one listing per row
columns:
- name: listing_id
description: primary key
data_tests:
- unique
- not_null
- name: stg_reviews
description: one review per row
columns:
- name: review_id
tests:
- unique
- not_null
- name: listing_id
tests:
- not_null
- name: stg_calendar
description: one row per date per listing
tests:
- dbt_utils.unique_combination_of_columns:
arguments:
combination_of_columns:
- listing_id
- calendar_date
- name: stg_hosts
description: one row per host
columns:
- name: host_id
tests:
- unique
- not_null
Why This YAML File Is More Important Than It Looks
At first glance, this looks like metadata.
But in reality, this file defines something much more powerful:
It defines what "correct data" means for the entire project.
Let's break it down.
1. Defining Uniqueness = Defining Identity
For example:
- name: listing_id
data_tests:
- unique
- not_null
This tells us:
"A listing must always be uniquely identifiable."
This seems obvious until you work with real-world data.
Because in messy datasets:
- IDs can be duplicated
- IDs can be missing
- IDs can change over time
By enforcing this rule, we are not just documenting data.
We are enforcing business identity rules.
2. Relationships Matter (Not Just Columns)
In the reviews model:
- name: listing_id
tests:
- not_null
This ensures that every review must belong to a listing.
This is subtle but important.
It prevents:
- Orphaned reviews
- Broken joins
- Misleading aggregations
Without this, downstream models like Host360 or Listing360 could silently produce incorrect results.
3. Composite Uniqueness: Calendar Data Is Special
The calendar model introduced something more interesting:
- dbt_utils.unique_combination_of_columns:
arguments:
combination_of_columns:
- listing_id
- calendar_date
This reflects a deeper concept:
Some datasets don't have a single primary key — their identity is composite.
In Airbnb's calendar data:
- A listing can appear many times
- Each row represents a specific date
- Meaning identity is defined by (listing_id + date)
This is a classic analytics modeling concept:
Grain definition matters more than columns.
4. Why Hosts Need Their Own Integrity Rules
For hosts:
- name: host_id
tests:
- unique
- not_null
This defines a clean assumption:
One host = one unique entity in the system.
This becomes critical later when building:
- Host360
- Performance metrics
- Listing aggregation
Because if host identity is not stable, every downstream metric becomes unreliable.
The Real Insight: dbt Tests Are Not Optional
At first, I treated these tests as optional checks.
Something to run "just in case".
But as the project grew, I realized:
These tests are not validation — they are design decisions.
They define:
- What the system considers valid data
- What assumptions downstream models can rely on
- How trust is established across the pipeline
Without them, every model becomes a guess.
Staging Layer After Adding Tests
After introducing both staging models and YAML tests, the architecture evolved again:
RAW (Snowflake)
↓
STAGING (dbt + tests)
↓
INTERMEDIATE MODELS
↓
MARTS (Host360, Listing360)
But now there is an important difference:
The staging layer is no longer just transformation — it is a validation boundary.
Why This Matters for the Rest of the Project
This becomes extremely important when we later build:
- Host360
- Relies on host uniqueness
- Assumes stable listing relationships
- Listing360
- Depends on valid listing IDs
- Assumes clean review linkage
- Executive Layer
- Assumes all upstream metrics are trustworthy
If staging tests are wrong or missing:
Every downstream model silently becomes unreliable.
What We Have Built So Far
At this point in the project:
- Raw Airbnb data is ingested into Snowflake
- A dbt project structure exists
- Staging models standardize raw data
- YAML tests enforce data integrity rules
We now have something very important:
A transformation layer that is not only structured — but also trustworthy.
Looking Ahead
In the next article, we'll move deeper into modeling:
Why Intermediate Models Are the Most Underestimated Layer in dbt Projects
This is where we define:
- What a "fact" actually is in this project
- How grain changes modeling decisions
- Why staging alone is not enough for analytics
And most importantly:
How raw data becomes a structured system that can support real business questions.