Chapter 03 · Dimensional Modeling Foundations: Facts, Dimensions, Star Schemas, and Query Semantics

Model a Sales/Orders Process as a Star Schema and Validate Common BI Queries Against the Declared Grain

Validate the complete AtlasMart sales star with executable BI queries, control totals, orphan checks, and grain-aware distinct-order logic.

Intermediate → Advanced110–130 minutesStar-schema acceptance suitePython 3.13.5 · SQLite 3.46.1 testedLast reviewed: September 2026

Learning outcomes

The Chapter 03 model is complete only when representative BI questions return the expected answers without violating the paid order-line grain. This lesson treats query correctness as executable acceptance criteria: raw fact controls, slice totals, distinct-order counts, orphan checks, and anti-double-counting tests must all agree.

01

Validate the canonical fact_sales population against the Chapter 02 controls.

02

Answer common date, segment, category, store, product, and order-level BI questions through dimensional joins.

03

Use COUNT(DISTINCT order_id) when the requested metric is order count at line grain.

04

Detect join multiplication and fact loss with explicit control-total assertions.

05

Package the star model with a repeatable acceptance test before proceeding to advanced fact-table design.

Executed baseline for this chapter

The local verification scripts in this generated chapter were executed with Python 3.13.5 and SQLite 3.46.1. They use only stable standard-library/SQL features. Learners should still record the versions printed on their machine; the exercises prove semantic contracts on the synthetic fixture, not performance of a production warehouse.

1. Re-state the acceptance contract before writing BI SQL

Invariant Expected value Reason
fact_sales rows 7 One row per paid order line
distinct paid orders 4 O1001, O1002, O1003, O1005
paid units 9 Sum of line quantities
paid GMV 625 Sum of line extended_amount
dimension orphans 0 Every fact FK resolves to one dimension row
pending O1004 rows in fact_sales 0 Pending orders are outside certified sales population

These controls precede any dashboard result. If a query cannot reconcile to them where it should, the problem is semantic even if the SQL runs successfully.

2. Validate daily and segment slices

SQL · daily paid GMV
SELECT d.full_date,       SUM(f.extended_amount) AS paid_gmv,       SUM(f.quantity) AS paid_units,       COUNT(DISTINCT f.order_id) AS paid_ordersFROM fact_sales f JOIN dim_date d ON d.date_key=f.date_keyGROUP BY d.full_date ORDER BY d.full_date;
Date GMV Units Orders
2026-09-18 325 4 2
2026-09-19 150 3 1
2026-09-20 150 2 1
SQL · customer-segment slice
SELECT c.segment,       SUM(f.extended_amount) AS paid_gmv,       SUM(f.quantity) AS paid_units,       COUNT(DISTINCT f.order_id) AS paid_ordersFROM fact_sales f JOIN dim_customer c ON c.customer_key=f.customer_keyGROUP BY c.segment ORDER BY c.segment;
Segment GMV Units Orders
Consumer 350 3 2
SMB 275 6 2

3. Validate product/category and selling-location slices

SQL · category performance
SELECT p.category,       SUM(f.extended_amount) AS paid_gmv,       SUM(f.quantity) AS paid_unitsFROM fact_sales f JOIN dim_product p ON p.product_key=f.product_keyGROUP BY p.category ORDER BY p.category;
Category GMV Units
Accessories 425 8
Displays 200 1
SQL · selling-location performance
SELECT s.store_name,       SUM(f.extended_amount) AS paid_gmv,       COUNT(DISTINCT f.order_id) AS paid_ordersFROM fact_sales f JOIN dim_store s ON s.store_key=f.store_keyGROUP BY s.store_name ORDER BY s.store_name;
Selling location GMV Orders
Mobile Store 350 2
Web Store 275 2

Assisted Sales has no paid fact rows because its only Chapter 01 order is pending. A dimension may legitimately contain members absent from a particular fact population.

4. Controlled failure: count fact rows as orders

At order-line grain, COUNT(*) answers “how many fact rows?” It does not answer “how many orders?”

SQL · wrong vs correct order count
SELECT COUNT(*) AS wrong_order_count FROM fact_sales;-- 7 line rows, not 7 ordersSELECT COUNT(DISTINCT order_id) AS paid_order_count FROM fact_sales;-- 4 paid orders

The same grain mistake appears in averages. AVG(extended_amount) is average line amount, not average order value. The latter requires first aggregating each order or dividing total paid GMV by the distinct-order count according to the metric contract.

5. Hands-on lab — run the Chapter 03 acceptance suite

Save the canonical SQL and this validator in a disposable directory.

validate_ch03.py
import sqlite3from pathlib import Pathconn=sqlite3.connect(":memory:")conn.execute("PRAGMA foreign_keys=ON")conn.executescript(Path("atlasmart_star.sql").read_text())def one(sql): return conn.execute(sql).fetchone()[0]assert one("SELECT COUNT(*) FROM fact_sales") == 7assert one("SELECT COUNT(DISTINCT order_id) FROM fact_sales") == 4assert one("SELECT SUM(quantity) FROM fact_sales") == 9assert one("SELECT SUM(extended_amount) FROM fact_sales") == 625# Reconciliation after joining all dimensions: no loss or multiplication.joined=one("""SELECT SUM(f.extended_amount)FROM fact_sales fJOIN dim_date d ON d.date_key=f.date_keyJOIN dim_customer c ON c.customer_key=f.customer_keyJOIN dim_product p ON p.product_key=f.product_keyJOIN dim_store s ON s.store_key=f.store_key""")assert joined == 625# Orphan assertions.for dim,key in [('dim_date','date_key'),('dim_customer','customer_key'),('dim_product','product_key'),('dim_store','store_key')]:    n=one(f"SELECT COUNT(*) FROM fact_sales f LEFT JOIN {dim} d ON d.{key}=f.{key} WHERE d.{key} IS NULL")    assert n == 0, (dim,n)# Slice controls.assert conn.execute("""SELECT p.category,SUM(f.extended_amount),SUM(f.quantity)FROM fact_sales f JOIN dim_product p ON p.product_key=f.product_keyGROUP BY p.category ORDER BY p.category""").fetchall() == [('Accessories',425,8),('Displays',200,1)]assert conn.execute("""SELECT s.store_name,SUM(f.extended_amount)FROM fact_sales f JOIN dim_store s ON s.store_key=f.store_keyGROUP BY s.store_name ORDER BY s.store_name""").fetchall() == [('Mobile Store',350),('Web Store',275)]# Deliberate anti-test: line count must not equal order count.line_count=one("SELECT COUNT(*) FROM fact_sales")order_count=one("SELECT COUNT(DISTINCT order_id) FROM fact_sales")assert line_count != order_count and (line_count,order_count)==(7,4)print("Chapter 03 acceptance tests: PASS")print("rows/orders/units/gmv =",7,4,9,625)

Expected output is Chapter 03 acceptance tests: PASS and the four controls. Break the model deliberately by changing one fact's product_key to another valid product key; the grand total still passes but the category slice assertion fails. This demonstrates why reconciliation needs both global controls and dimensional slice tests.

Cleanup: remove the two disposable files.

6. A star schema does not eliminate semantic review

The SQL can be syntactically correct and still answer the wrong question. Analysts must know that the fact population is paid lines, that order count is distinct order_id, that unit price is not additive, that store is the Chapter 03 selling-location mapping, and that historical dimension behavior has not yet been introduced. Documentation and metadata should expose those contracts to BI consumers.

This is why a dimensional model is not merely a faster join pattern. It is an interface for analytical meaning.

7. Production judgment and bridge

Before promoting a first star, validate event coverage, grain uniqueness, dimension lookup coverage, global controls, representative slices, source lineage, metric formulas, access sensitivity, and the failure behavior of missing context. Keep physical optimization separate so a later engine migration cannot silently redefine metrics.

Chapter 04 now deepens fact-table design: additive, semi-additive, and non-additive measures plus factless facts. The seven-line sales fact remains the baseline for those aggregation rules.

Knowledge check

Check your understanding

  1. Why is COUNT(*) wrong for paid_order_count in fact_sales?
  2. What are the expected Chapter 03 category totals?
  3. Why does the validation suite check both grand totals and slices?
  4. What does zero orphan count establish?
  5. What important dimension behavior has Chapter 03 not implemented yet?
Review the answers

1. Because the fact grain is order line, so COUNT(*) counts 7 lines; paid order count is 4 distinct order_id values.

2. Accessories = 425 GMV and 8 units; Displays = 200 GMV and 1 unit.

3. A wrong dimension key can preserve the grand total while moving value into the wrong slice; both levels are needed.

4. Every fact foreign key resolves to a matching dimension member for the tested dimensions.

5. Historical/slowly changing dimension versioning; Chapter 03 uses one current descriptive row per business key plus explicit Unknown members.

Summary and next step

AtlasMart's first star schema now passes executable acceptance criteria at its declared grain. Facts, dimensions, surrogate keys, unknown members, and joins preserve 625 paid GMV, 9 units, four paid orders, and the expected dimensional slices.

Next: classify measures by aggregation behavior and design fact tables that remain correct across time and other dimensions.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.