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

6 May 2026 8 min read

Last updated: 1 October 2026

Share:

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.

python
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()
text
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: True

What 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

ModelStrengthsCostsFits
StarSimple, fast queries; understood by BI tools and peopleDimension loads carry the integration logicThe presentation layer of almost every warehouse
SnowflakeLess redundancy in large dimensions; mirrors source hierarchiesMore joins, harder to navigateVery large dimensions, or tools that need normalised hierarchies
Data vaultAbsorbs new sources and changes; full audit trail; parallel loadsMany tables; not queried directlyThe 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:

Is star schema better than snowflake schema?

For reporting, usually yes. A star's flat dimensions hold the same information with fewer joins and are easier for people and BI tools to use.

What is a data vault used for?

Integrating many source systems with full history and auditability. It is loaded easily and extended without rework, then exposed through star schemas for querying.

Can I query a data vault directly?

You can, but it takes many joins. Most teams build views or star-schema marts on top for analysts.

Which model should a small team choose?

A star schema. Add an integration layer only when the number of sources and the rate of change make dimension loads hard to maintain.

Data modelling techniques, star and snowflake schemas and data warehouse concepts are part of the data warehousing module of the Data Engineering course.

Sources

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.

Explore course