Data Modeling: Star Schema, Snowflake and Data Vault
The three main warehouse modelling approaches compared by building each from the real Chinook database: a star with 3 tables, a snowflake with 6 and a data vault with 11, the joins the same question needs in each, why all three give identical answers, what each optimises for, and how teams combine them.
GloryTecks Team
Training & Placement Faculty
Last updated: 1 October 2026
Data modelling decides how a warehouse's tables are shaped, and therefore how easy it is to query, how well it absorbs change, and how much history it keeps. Three approaches dominate: the star schema, its normalised cousin the snowflake schema, and the data vault. They are easiest to understand side by side, built from the same data.
The three approaches
Star schema. Fact tables linked to dimension tables by keys (Kimball Group: star schemas). Dimensions are flat: a product row carries its category, brand and supplier. It is designed for the people and tools that query it.
Snowflake schema. A star whose dimension hierarchies are normalised into secondary tables, so category sits in its own table referenced from product. Kimball advises against it: snowflakes are harder for business users to navigate and can hurt query performance, while a flattened dimension holds exactly the same information (Kimball Group: snowflaked dimensions).
Data vault. A pattern for integrating many sources, built from three kinds of table (Databricks: data vault):
- Hubs hold business keys, such as a customer number, with load metadata.
- Links hold relationships between hubs.
- Satellites hold descriptive attributes and their history.
It is designed for loading and auditing, not for querying directly.
One dataset, three models
The script builds all three from the Chinook music store database (Chinook on GitHub), then asks each the same question: revenue by genre and customer country.
import shutil
import sqlite3
import urllib.request
from pathlib import Path
url = "https://github.com/lerocha/chinook-database/releases/download/v1.4.5/Chinook_Sqlite.sqlite"
if not Path("chinook.sqlite").exists():
urllib.request.urlretrieve(url, "chinook.sqlite")
shutil.copy("chinook.sqlite", "work.sqlite")
con = sqlite3.connect("work.sqlite")
con.executescript("""
-- star: one fact table, flat dimensions
CREATE TABLE star_fact_sales AS
SELECT il.InvoiceLineId, i.CustomerId AS customer_key, il.TrackId AS track_key, date(i.InvoiceDate) AS date_key,
il.Quantity AS quantity, il.UnitPrice * il.Quantity AS amount
FROM InvoiceLine il JOIN Invoice i USING (InvoiceId);
CREATE TABLE star_dim_track AS
SELECT t.TrackId AS track_key, t.Name AS track, al.Title AS album, ar.Name AS artist, g.Name AS genre
FROM Track t JOIN Album al USING (AlbumId) JOIN Artist ar USING (ArtistId) JOIN Genre g USING (GenreId);
CREATE TABLE star_dim_customer AS SELECT CustomerId AS customer_key, Country AS country FROM Customer;
-- snowflake: the same fact table, but the track dimension's hierarchy is normalised
CREATE TABLE snow_dim_track AS SELECT TrackId AS track_key, Name AS track, AlbumId AS album_key, GenreId AS genre_key FROM Track;
CREATE TABLE snow_dim_album AS SELECT AlbumId AS album_key, Title AS album, ArtistId AS artist_key FROM Album;
CREATE TABLE snow_dim_artist AS SELECT ArtistId AS artist_key, Name AS artist FROM Artist;
CREATE TABLE snow_dim_genre AS SELECT GenreId AS genre_key, Name AS genre FROM Genre;
-- data vault: hubs hold business keys, links hold relationships, satellites hold descriptive data
CREATE TABLE hub_customer AS SELECT CustomerId AS customer_bk, '2013-12-31' AS load_date, 'chinook' AS source FROM Customer;
CREATE TABLE hub_track AS SELECT TrackId AS track_bk, '2013-12-31' AS load_date, 'chinook' AS source FROM Track;
CREATE TABLE hub_genre AS SELECT GenreId AS genre_bk, '2013-12-31' AS load_date, 'chinook' AS source FROM Genre;
CREATE TABLE hub_invoice AS SELECT InvoiceId AS invoice_bk, '2013-12-31' AS load_date, 'chinook' AS source FROM Invoice;
CREATE TABLE sat_customer AS SELECT CustomerId AS customer_bk, Country AS country, '2013-12-31' AS load_date FROM Customer;
CREATE TABLE sat_genre AS SELECT GenreId AS genre_bk, Name AS genre, '2013-12-31' AS load_date FROM Genre;
CREATE TABLE sat_invoice AS SELECT InvoiceId AS invoice_bk, date(InvoiceDate) AS invoice_date, '2013-12-31' AS load_date FROM Invoice;
CREATE TABLE link_track_genre AS SELECT TrackId AS track_bk, GenreId AS genre_bk FROM Track;
CREATE TABLE link_invoice_customer AS SELECT InvoiceId AS invoice_bk, CustomerId AS customer_bk FROM Invoice;
CREATE TABLE link_invoice_track AS SELECT InvoiceLineId AS line_bk, InvoiceId AS invoice_bk, TrackId AS track_bk FROM InvoiceLine;
CREATE TABLE sat_invoice_track AS SELECT InvoiceLineId AS line_bk, Quantity AS quantity, UnitPrice * Quantity AS amount FROM InvoiceLine;
""")
SELECT = "SELECT genre, country, ROUND(SUM(amount), 2) AS revenue FROM "
TAIL = " GROUP BY genre, country ORDER BY revenue DESC, genre, country LIMIT 3"
queries = {
"star": SELECT + """star_fact_sales f JOIN star_dim_track t USING (track_key) JOIN star_dim_customer c USING (customer_key)""" + TAIL,
"snowflake": SELECT + """star_fact_sales f JOIN snow_dim_track t USING (track_key) JOIN snow_dim_genre g USING (genre_key)
JOIN star_dim_customer c USING (customer_key)""" + TAIL,
"data vault": SELECT + """link_invoice_track l JOIN sat_invoice_track s USING (line_bk)
JOIN link_track_genre tg USING (track_bk) JOIN sat_genre g USING (genre_bk)
JOIN link_invoice_customer ic USING (invoice_bk) JOIN sat_customer c USING (customer_bk)""" + TAIL,
}
tables = [r[0] for r in con.execute("SELECT name FROM sqlite_master WHERE type = 'table'")]
count = lambda prefixes: sum(t.startswith(prefixes) for t in tables)
print("tables: star", count("star_"), "| snowflake", count(("star_fact", "star_dim_customer", "snow_")),
"| data vault", count(("hub_", "link_", "sat_")))
results = {name: con.execute(sql).fetchall() for name, sql in queries.items()}
for name, sql in queries.items():
print(f"{name:<11} joins in the query: {sql.count('JOIN')}")
print("top rows:", results["star"])
print("all three agree:", results["star"] == results["snowflake"] == results["data vault"])
con.close()tables: star 3 | snowflake 6 | data vault 11
star joins in the query: 2
snowflake joins in the query: 3
data vault joins in the query: 5
top rows: [('Rock', 'USA', 155.43), ('Rock', 'Canada', 105.93), ('Latin', 'USA', 90.09)]
all three agree: TrueWhat the comparison shows
The answers are identical. Modelling changes structure, not facts. All three return the same top rows, which is the first thing to test when converting between models.
Complexity for the query writer grows from star to vault. The star needs 3 tables and 2 joins; the snowflake 6 tables and 3 joins, because genre now lives a table away from track; the vault 11 tables and 5 joins, because every relationship is a link and every description a satellite. A real vault adds hash keys, load dates and record sources to every table, and history rows to every satellite, so queries get harder still.
The models trade query ease for load flexibility. In the vault, a new source system for customers means adding a satellite, with no change to existing tables, and hubs, links and satellites can load in parallel with simple, repeatable patterns. In the star, the same change means reworking a dimension's load. That is why the Databricks description places the vault in the integration layer and expects friendlier tables on top.
When to use each
| Model | Strengths | Costs | Fits |
|---|---|---|---|
| Star | Simple, fast queries; understood by BI tools and people | Dimension loads carry the integration logic | The presentation layer of almost every warehouse |
| Snowflake | Less redundancy in large dimensions; mirrors source hierarchies | More joins, harder to navigate | Very large dimensions, or tools that need normalised hierarchies |
| Data vault | Absorbs new sources and changes; full audit trail; parallel loads | Many tables; not queried directly | The integration layer of large, multi-source enterprises |
A small team with a handful of sources is usually best served by stars alone. A large organisation with dozens of sources, frequent change and audit requirements may justify a vault, with star schemas built from it for reporting.
Two more shapes you will meet
The normalised enterprise model. Some warehouses integrate data in third normal form, much like an operational database but spanning all sources and keeping history, then build star-schema marts from it. It minimises redundancy and suits organisations with strong central data modelling, at the cost of a long design phase before anything is delivered.
One big table. Columnar warehouses made it practical to pre-join a fact with all its dimensions into a single wide table. Queries need no joins at all, which is convenient for a specific dashboard or a machine learning feature set. The costs are those of denormalising taken to the limit: every descriptive value is repeated on every row, a change to one attribute means rewriting many rows, and different wide tables can drift apart. Wide tables work best as outputs generated from a star, not as the model of record.
How they combine in practice
Modern warehouses layer their models. Raw data lands unchanged; an integration layer, sometimes a data vault, sometimes a set of cleaned, lightly modelled tables, reconciles sources; and star schemas form the presentation layer that analysts and BI tools use. In a medallion architecture the same idea appears as bronze, silver and gold. The principle is to model each layer for its job: integration layers for change and audit, presentation layers for understanding and speed.
Getting the details right
Whichever approach you choose, results depend on the details of table design:
- Grain, additivity and dimension techniques: designing fact and dimension tables.
- History: slowly changing dimensions.
- A worked build: how to build a data warehouse.
- BI-side modelling: star schema in Power BI.
Is star schema better than snowflake schema?
What is a data vault used for?
Can I query a data vault directly?
Which model should a small team choose?
Data modelling techniques, star and snowflake schemas and data warehouse concepts are part of the data warehousing module of the Data Engineering course.
Sources
- Star schemas and OLAP cubes (Kimball Group) — Kimball Group
- Snowflaked dimensions (Kimball Group) — Kimball Group
- Data vault (Databricks glossary) — Databricks
- Chinook sample database (GitHub) — GitHub
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Data Engineering?
Join GloryTecks’ expert-led Data Engineering training in Hyderabad with real-time projects and 100% placement support.
Related articles
Data Engineering Roadmap 2026: A Complete Guide
A stage-by-stage data engineering roadmap for 2026: what the role involves according to current certification guides, then seven stages, SQL, Python, pipelines and modelling, orchestration and quality, Spark and big data, streaming, and cloud platforms, each with a checkpoint, and detours to avoid.
Data Warehouse Explained: Concepts and Architecture
A clear explanation of data warehouses: why analytical queries need a separate store from transactional systems, the architecture from sources through staging, modelled layers and a semantic model, facts, dimensions, grain and history, modelling approaches, modern platforms and common pitfalls.
Data Lake vs Data Warehouse vs Lakehouse
A clear comparison of data lakes, data warehouses and lakehouses: what each stores and when structure is applied, strengths and failure modes such as the data swamp, what a lakehouse adds through table formats and governance, a side-by-side table, and practical guidance on choosing or combining them.