0Pricing
SQL Academy · Lesson

Star and Snowflake Schemas

Model data for fast analytics.

Star and Snowflake Schemas is a free SQL Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the SQL Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is a Data Warehouse Schema?

In a transactional (OLTP) database you normalize data to avoid redundancy. In a data warehouse you often denormalize intentionally — trading storage for query speed. Two classic patterns for organizing warehouse tables are the Star Schema and the Snowflake Schema.

Both revolve around a central fact table surrounded by dimension tables. The difference is how far you normalize those dimensions.

Fact Tables and Dimension Tables

A fact table stores measurable events — sales, clicks, shipments. It is wide (many rows) and contains numeric measures plus foreign keys to dimensions.

A dimension table describes the context of each event: who, what, when, where. Dimensions are narrower (fewer rows) but richer in descriptive columns.

CREATE TABLE fact_sales (
  sale_id      SERIAL PRIMARY KEY,
  date_key     INT NOT NULL,
  product_key  INT NOT NULL,
  customer_key INT NOT NULL,
  store_key    INT NOT NULL,
  quantity     INT NOT NULL,
  revenue      NUMERIC(12, 2) NOT NULL
);

CREATE TABLE dim_product (
  product_key  SERIAL PRIMARY KEY,
  product_name VARCHAR(200),
  category     VARCHAR(100),
  brand        VARCHAR(100),
  unit_price   NUMERIC(10, 2)
);

The Star Schema

In a star schema every dimension table connects directly to the fact table. Draw the relationships on paper and it looks like a star — the fact table is the center, dimensions are the points.

Dimension tables are fully denormalized: all descriptive attributes live in a single table, even if some attributes repeat across rows.

-- Star schema: all product info in one flat dimension table
CREATE TABLE dim_product (
  product_key    SERIAL PRIMARY KEY,
  product_name   VARCHAR(200),
  category_name  VARCHAR(100),   -- denormalized
  subcategory    VARCHAR(100),   -- denormalized
  brand_name     VARCHAR(100),   -- denormalized
  brand_country  VARCHAR(100),   -- denormalized
  unit_price     NUMERIC(10, 2)
);

CREATE TABLE dim_date (
  date_key   INT PRIMARY KEY,   -- e.g. 20240315
  full_date  DATE,
  year       INT,
  quarter    INT,
  month      INT,
  month_name VARCHAR(20),
  week       INT,
  day_of_week VARCHAR(10)
);

Star Schema Query

The flat dimension tables make queries simple. You join the fact table to one or more dimensions and aggregate. There are no secondary joins through chains of normalized tables.

This is why star schemas deliver fast analytical queries — the join graph is shallow.

SELECT
  d.year,
  d.quarter,
  p.category_name,
  SUM(f.revenue)   AS total_revenue,
  SUM(f.quantity)  AS units_sold
FROM fact_sales f
JOIN dim_date    d ON d.date_key    = f.date_key
JOIN dim_product p ON p.product_key = f.product_key
WHERE d.year = 2024
GROUP BY d.year, d.quarter, p.category_name
ORDER BY d.quarter, total_revenue DESC;

The Snowflake Schema

A snowflake schema normalizes dimension tables further by splitting them into sub-dimensions. For example, instead of storing category_name and brand_name inside dim_product, you create separate dim_category and dim_brand tables.

The resulting diagram looks like a snowflake — branching arms of related tables.

-- Snowflake schema: product dimension is normalized
CREATE TABLE dim_brand (
  brand_key     SERIAL PRIMARY KEY,
  brand_name    VARCHAR(100),
  brand_country VARCHAR(100)
);

CREATE TABLE dim_category (
  category_key   SERIAL PRIMARY KEY,
  category_name  VARCHAR(100),
  subcategory    VARCHAR(100)
);

CREATE TABLE dim_product (
  product_key  SERIAL PRIMARY KEY,
  product_name VARCHAR(200),
  category_key INT REFERENCES dim_category(category_key),
  brand_key    INT REFERENCES dim_brand(brand_key),
  unit_price   NUMERIC(10, 2)
);

Snowflake Schema Query

Querying a snowflake schema requires more joins to reassemble dimension data that was split across tables. The query optimizer must traverse the extra levels, which can add latency compared to a star schema.

However, the normalized dimensions are smaller and consistent — updating a brand name in one row of dim_brand automatically applies everywhere.

SELECT
  d.year,
  c.category_name,
  b.brand_name,
  SUM(f.revenue) AS total_revenue
FROM fact_sales    f
JOIN dim_date      d ON d.date_key    = f.date_key
JOIN dim_product   p ON p.product_key = f.product_key
JOIN dim_category  c ON c.category_key = p.category_key
JOIN dim_brand     b ON b.brand_key    = p.brand_key
WHERE d.year = 2024
GROUP BY d.year, c.category_name, b.brand_name
ORDER BY total_revenue DESC;

Surrogate Keys vs Natural Keys

Dimension tables typically use a surrogate key — an integer generated by the warehouse (e.g. SERIAL) — rather than a natural key from the source system.

Surrogate keys are stable even when the source changes, they are compact for large fact tables, and they support slowly changing dimensions where history must be tracked.

-- Surrogate key (product_key) vs natural key (sku)
INSERT INTO dim_product (product_name, category_key, brand_key, unit_price)
VALUES ('Wireless Headphones', 3, 7, 89.99);
-- product_key is assigned by SERIAL -- the natural key (SKU) lives elsewhere

-- Natural key would be:
-- INSERT INTO dim_product (sku, product_name, ...)
-- VALUES ('WH-1000XM5', 'Wireless Headphones', ...);
-- Risky: SKU can be reused or reassigned by the source system

The Date Dimension

The date dimension is special — it is almost always present and is usually pre-populated for many years of dates. Storing derived attributes (year, quarter, month name, fiscal period, holiday flag) in the dimension table avoids recomputing them at query time.

-- Populate dim_date for one year using generate_series
INSERT INTO dim_date (date_key, full_date, year, quarter, month, month_name, week, day_of_week)
SELECT
  TO_CHAR(d, 'YYYYMMDD')::INT  AS date_key,
  d                             AS full_date,
  EXTRACT(YEAR    FROM d)::INT  AS year,
  EXTRACT(QUARTER FROM d)::INT  AS quarter,
  EXTRACT(MONTH   FROM d)::INT  AS month,
  TO_CHAR(d, 'Month')           AS month_name,
  EXTRACT(WEEK    FROM d)::INT  AS week,
  TO_CHAR(d, 'Day')             AS day_of_week
FROM generate_series('2024-01-01'::DATE, '2024-12-31'::DATE, '1 day') AS d;

Slowly Changing Dimensions (SCD Type 2)

What happens when a customer moves cities or a product changes category? You need to track history. SCD Type 2 inserts a new dimension row for each change while closing the previous one with an end date. The fact table row still points to the old dimension key, preserving historical accuracy.

-- SCD Type 2 customer dimension
CREATE TABLE dim_customer (
  customer_key  SERIAL PRIMARY KEY,
  customer_id   INT NOT NULL,
  customer_name VARCHAR(200),
  city          VARCHAR(100),
  country       VARCHAR(100),
  valid_from    DATE NOT NULL,
  valid_to      DATE,
  is_current    BOOLEAN DEFAULT TRUE
);

-- When a customer moves, close old row and insert new one:
UPDATE dim_customer
   SET valid_to = CURRENT_DATE - 1, is_current = FALSE
 WHERE customer_id = 42 AND is_current = TRUE;

INSERT INTO dim_customer (customer_id, customer_name, city, country, valid_from, is_current)
VALUES (42, 'Alice Muller', 'Berlin', 'Germany', CURRENT_DATE, TRUE);

Star vs Snowflake — Trade-offs

Neither schema is universally better. Choose based on your priorities:

  • Star — fewer joins, faster queries, simpler ETL, higher storage cost. Best for read-heavy analytics tools (Tableau, Power BI).
  • Snowflake — normalized dimensions, less redundancy, easier dimension updates, but more joins. Better when dimensions are large or shared across multiple fact tables.
-- Checking how much storage the denormalized category column costs
-- in a large dim_product (star schema) vs a separate dim_category (snowflake)
SELECT
  COUNT(*)                               AS total_products,
  COUNT(DISTINCT category_name)          AS unique_categories,
  pg_size_pretty(
    SUM(pg_column_size(category_name))
  )                                      AS category_storage
FROM dim_product;

Galaxy Schema (Fact Constellation)

When a warehouse has multiple fact tables that share dimension tables, the result is called a galaxy schema (or fact constellation). For example, a retail warehouse might have separate fact tables for sales and returns, both referencing the same dim_product and dim_date.

Shared dimensions enforce consistent filtering and make cross-fact comparisons straightforward.

CREATE TABLE fact_returns (
  return_id     SERIAL PRIMARY KEY,
  date_key      INT NOT NULL REFERENCES dim_date(date_key),
  product_key   INT NOT NULL REFERENCES dim_product(product_key),
  customer_key  INT NOT NULL,
  quantity      INT NOT NULL,
  refund_amount NUMERIC(12, 2) NOT NULL
);

-- Cross-fact query: net revenue = sales - refunds
SELECT
  d.year,
  d.month,
  SUM(s.revenue)       AS gross_revenue,
  SUM(r.refund_amount) AS total_refunds,
  SUM(s.revenue) - COALESCE(SUM(r.refund_amount), 0) AS net_revenue
FROM dim_date d
LEFT JOIN fact_sales   s ON s.date_key = d.date_key
LEFT JOIN fact_returns r ON r.date_key = d.date_key
WHERE d.year = 2024
GROUP BY d.year, d.month
ORDER BY d.month;

Star vs Snowflake Schema

Test your understanding of star and snowflake schemas.

Lesson Recap

In this lesson you explored two foundational data warehouse design patterns:

  • Star Schema — a central fact table surrounded by flat, denormalized dimension tables. Fewer joins, faster queries, slightly more storage.
  • Snowflake Schema — dimension tables are further normalized into sub-dimensions. Less redundancy, easier updates, but more joins required.
  • Fact tables hold measurable events; dimension tables provide context (who, what, when, where).
  • Surrogate keys protect historical accuracy and decouple the warehouse from source system changes.
  • SCD Type 2 tracks dimension history by adding new rows with validity dates instead of overwriting old ones.
  • When multiple fact tables share dimensions, the design becomes a galaxy (fact constellation) schema.

Choose star for simplicity and speed; choose snowflake when dimensions are large, frequently updated, or shared across many fact tables.

Frequently asked questions

Is the “Star and Snowflake Schemas” lesson free?

Yes — the full text of “Star and Snowflake Schemas” is free to read here on the web, and the SQL Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the SQL Academy course, upgrade to CoddyKit PRO.

What will I learn in “Star and Snowflake Schemas”?

Model data for fast analytics. You practise SQL Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start SQL Academy?

No prior experience is required. SQL Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Star and Snowflake Schemas” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this SQL Academy lesson?

Yes. Every SQL Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. OLTP vs OLAP
  2. Fact and Dimension Tables
  3. Star and Snowflake Schemas
  4. Writing Analytical Queries
← Back to SQL Academy