See it as a diagram
Everything below, as a diagram you can edit. Describe yours and see it in seconds.
No account needed · Editable canvas, not a picture
1. What Is Dimensional Modeling?
Dimensional modeling is a technique for designing data warehouses that prioritizes query simplicity and performance over storage efficiency. Created by Ralph Kimball in the 1990s, it remains the gold standard for analytics because it models data the way business users think about it.
Why Not Just Use 3NF?
Third Normal Form (3NF) is great for OLTP systems—it minimizes redundancy and prevents update anomalies. But it's terrible for analytics: queries require 15+ joins, performance degrades, and analysts can't understand the model without a PhD in your schema.
The core principle: separate the "what happened" from the "context"
Fact Tables
Store events and measurements: orders placed, clicks, payments. Contain metrics you can aggregate (sum, count, average).
Dimension Tables
Store descriptive context: who (customers), what (products), when (dates), where (locations). Enable filtering and grouping.
The Kimball philosophy
"The data warehouse is only as good as the business intelligence it enables." Dimensional models are designed for humans first, computers second. If an analyst can't write a query without help, the model has failed.
2. Facts vs Dimensions
The fundamental building blocks of dimensional modeling. Get this right and everything else follows.
Fact Tables
- Record business events/transactions
- Contain numeric measures (amount, quantity, duration)
- Have foreign keys to dimension tables
- Very tall and narrow (many rows, few columns)
- Grain = one row per event
Examples:
fct_orders, fct_page_views, fct_payments
Dimension Tables
- Store descriptive attributes
- Contain text fields for filtering/grouping
- Have a surrogate key as primary key
- Short and wide (fewer rows, many columns)
- Grain = one row per entity
Examples:
dim_customers, dim_products, dim_date
| Question | If Answer Is... | It's a... |
|---|---|---|
| Can you SUM/COUNT/AVG it? | Yes | Fact |
| Would you GROUP BY or FILTER on it? | Yes | Dimension |
| Does it describe an entity? | Yes | Dimension |
| Does it record an event/transaction? | Yes | Fact |
Example: E-commerce Order
-- Fact table: one row per order line item
CREATE TABLE fct_order_lines (
order_line_sk BIGINT PRIMARY KEY, -- Surrogate key
order_id VARCHAR(50), -- Natural key (degenerate dimension)
customer_sk BIGINT REFERENCES dim_customers,
product_sk BIGINT REFERENCES dim_products,
date_sk INT REFERENCES dim_date,
-- Measures (aggregatable)
quantity INT,
unit_price DECIMAL(10,2),
discount_amount DECIMAL(10,2),
line_total DECIMAL(10,2)
);
-- Dimension table: one row per customer
CREATE TABLE dim_customers (
customer_sk BIGINT PRIMARY KEY, -- Surrogate key
customer_id VARCHAR(50), -- Natural key
customer_name VARCHAR(255),
email VARCHAR(255),
segment VARCHAR(50), -- 'Enterprise', 'SMB', 'Consumer'
acquisition_channel VARCHAR(50),
created_at TIMESTAMP
);3. Star Schema Design
A star schema places a fact table at the center, surrounded by dimension tables. The name comes from the shape: fact in the middle, dimensions radiating outward like points of a star.
Star Schema Structure
dim_date
dim_customer
fct_orders
dim_product
dim_store
Fact table at center, dimensions around it — simple joins, intuitive queries
Star vs Snowflake Schema
Star Schema (Preferred)
- • Dimensions are denormalized
- • Fewer joins = faster queries
- • Easier to understand and query
- • Slight data redundancy (OK for analytics)
Snowflake Schema
- • Dimensions are normalized
- • More joins = slower queries
- • Harder to query without documentation
- • Saves storage (rarely matters today)
Pro Tip
When to Snowflake
Only snowflake when dimensions are truly hierarchical AND analysts frequently query at different levels independently (e.g., product → category → department). Otherwise, denormalize everything into the dimension table.
4. Types of Dimensions
Not all dimensions are created equal. Understanding these patterns helps you model correctly from the start.
Conformed Dimensions
Shared across multiple fact tables. dim_date, dim_customer used by fct_orders, fct_page_views, fct_support_tickets. The key to enterprise consistency.
Role-Playing Dimensions
Same dimension used multiple times with different meanings. dim_date joined as order_date, ship_date, delivery_date. Create views or aliases.
Degenerate Dimensions
Dimension keys stored in the fact table without a separate dimension table. Order numbers, invoice IDs, transaction IDs. No attributes worth storing separately.
Junk Dimensions
Combines miscellaneous low-cardinality flags into one dimension. Instead of 5 boolean columns in the fact table, create dim_order_flags with all combinations.
Date Dimension
The most important dimension. Pre-calculated attributes: day_of_week, is_weekend, fiscal_quarter, holiday_flag. Always use integer surrogate keys (YYYYMMDD format).
Example: Date Dimension
CREATE TABLE dim_date (
date_sk INT PRIMARY KEY, -- YYYYMMDD format
date_actual DATE NOT NULL,
day_of_week VARCHAR(10), -- 'Monday', 'Tuesday', ...
day_of_week_num INT, -- 1-7
day_of_month INT,
day_of_year INT,
week_of_year INT,
month_num INT,
month_name VARCHAR(10),
quarter_num INT,
quarter_name VARCHAR(10), -- 'Q1', 'Q2', ...
year_num INT,
fiscal_year INT,
fiscal_quarter INT,
is_weekend BOOLEAN,
is_holiday BOOLEAN,
holiday_name VARCHAR(50)
);
-- Usage: easily filter/group by any date attribute
SELECT
d.month_name,
d.year_num,
SUM(f.order_total) as revenue
FROM fct_orders f
JOIN dim_date d ON f.order_date_sk = d.date_sk
WHERE d.is_weekend = FALSE
GROUP BY d.month_name, d.year_num;5. Slowly Changing Dimensions (SCD)
Dimension attributes change over time. A customer moves, a product gets recategorized, an employee changes departments. How you handle these changes is critical for historical accuracy.
Type 0: Fixed
Never changes. Original value preserved forever. Use for attributes that shouldn't change (original signup date, birth date).
Example: Customer's original acquisition channel never updates even if they later come through a different channel.
Type 1: Overwrite
Old value is replaced with new. No history preserved. Use for corrections and attributes where history doesn't matter.
Tradeoff: Simple but you lose the ability to answer "what was the customer's segment when they placed this order?"
Type 2: Add New Row (Most Common)
Create a new dimension row with new surrogate key. Track with effective_date, expiry_date, is_current flag. Full history preserved.
Best for: Any attribute where historical accuracy matters. Segment changes, address changes, pricing tier changes.
Type 3: Add New Column
Add columns for current_value and previous_value. Limited history (typically just one prior value).
Rare: Only useful when you need exactly "before and after" comparison. Type 2 is almost always better.
Example: SCD Type 2 Implementation
-- SCD Type 2: Customer changes segment from 'SMB' to 'Enterprise'
-- BEFORE: 1 row
customer_sk | customer_id | segment | effective_date | expiry_date | is_current
1 | C001 | SMB | 2023-01-01 | 9999-12-31 | TRUE
-- AFTER: 2 rows (old row expired, new row added)
customer_sk | customer_id | segment | effective_date | expiry_date | is_current
1 | C001 | SMB | 2023-01-01 | 2024-06-15 | FALSE
2 | C001 | Enterprise | 2024-06-15 | 9999-12-31 | TRUE
-- Historical query: What segment was customer in when they ordered?
SELECT
o.order_id,
c.segment as segment_at_order_time
FROM fct_orders o
JOIN dim_customers c ON o.customer_sk = c.customer_sk
-- customer_sk in fact table points to correct historical versionSCD Type 2 Gotcha
When using Type 2, you must decide at load time which surrogate key to assign to the fact. Usually you want the dimension version that was "current" at the time of the event. This requires a point-in-time lookup during ETL.
6. Types of Fact Tables
Different business processes call for different fact table designs. Choose the right type based on the nature of your events.
Transaction Facts
One row per event at the lowest grain. Most common type. Orders, clicks, payments, logins.
Grain: One row per order line item
Periodic Snapshot Facts
One row per time period. Captures state at regular intervals. Account balances, inventory levels, pipeline snapshots.
Grain: One row per account per day
Accumulating Snapshot Facts
One row per process instance, updated as milestones occur. Order fulfillment, loan applications, support tickets.
Grain: One row per order (updated through lifecycle)
Factless Facts
Records events with no measures—just foreign keys. Student attendance, product promotions coverage.
Use: What students attended class? What products were on promotion?
Example: Accumulating Snapshot (Order Fulfillment)
CREATE TABLE fct_order_fulfillment (
order_sk BIGINT PRIMARY KEY,
order_id VARCHAR(50),
customer_sk BIGINT,
-- Multiple date foreign keys (milestones)
order_date_sk INT,
payment_date_sk INT,
ship_date_sk INT,
delivery_date_sk INT,
-- Lag measures (calculated)
days_to_payment INT,
days_to_ship INT,
days_to_delivery INT,
-- Measures
order_total DECIMAL(10,2),
current_status VARCHAR(50)
);
-- Row is updated as order progresses through fulfillment
-- Initially: only order_date_sk populated
-- After payment: payment_date_sk filled, days_to_payment calculated
-- After shipping: ship_date_sk filled, days_to_ship calculated
-- After delivery: delivery_date_sk filled, days_to_delivery calculated7. Best Practices Checklist
Define grain first
Before designing any fact table, explicitly state the grain: "One row per order line item" or "One row per customer per day." Never mix grains.
Use surrogate keys
Integer surrogate keys on all dimension tables. Store natural keys as attributes. This handles SCD Type 2 correctly and improves join performance.
Build conformed dimensions
dim_date and dim_customer should be shared across all fact tables. Same keys, same attributes. This enables cross-process analysis.
Denormalize dimensions
Prefer star over snowflake. Include category, department, region directly in the dimension. Storage is cheap, joins are expensive.
Add a date dimension
Never join on raw dates. Create dim_date with pre-calculated attributes. Use integer keys (YYYYMMDD) for partition pruning.
Handle nulls with default rows
Create "Unknown" or "Not Applicable" rows in dimensions (SK = -1). Never have null foreign keys in fact tables.
Document the grain
Every fact table should have its grain documented. This prevents accidental double-counting and helps analysts write correct queries.
Use SCD Type 2 for important attributes
Customer segment, product category, employee department—anything that changes and affects analysis should be Type 2.
Pro Tip
The "Analyst Test"
After designing your model, have an analyst write 5 common queries without documentation. If they can do it in under 5 minutes each, your model is good. If they need to ask questions or make mistakes, simplify your design.
8. Frequently Asked Questions
What is the difference between a fact table and a dimension table?
Fact tables store measurable business events (transactions, clicks, orders) with numeric metrics and foreign keys. Dimension tables store descriptive attributes (customer names, product categories, dates) that provide context for analyzing facts. Facts answer "how much" while dimensions answer "who, what, when, where, why."
What is a star schema vs snowflake schema?
A star schema has denormalized dimension tables directly connected to a central fact table, forming a star shape. A snowflake schema normalizes dimensions into sub-dimensions (e.g., product → category → department). Star schemas are preferred for analytics because they're simpler to query and perform better.
What are slowly changing dimensions (SCD)?
Slowly changing dimensions handle attribute changes over time. Type 1 overwrites the old value (no history). Type 2 creates a new row with effective dates (full history). Type 3 adds columns for previous values (limited history). Type 2 is most common for analytics requiring historical accuracy.
Should I use surrogate keys or natural keys?
Use surrogate keys (auto-generated integers) as primary keys in dimension tables. Natural keys (business identifiers like customer_id) should be stored as attributes. Surrogate keys are stable, performant, and handle SCD Type 2 correctly. Natural keys can change and cause join issues.
Visualize Your Dimensional Model
Create clear star schema diagrams showing facts, dimensions, and relationships. Document your data model so analysts can query it without asking questions.
Related Guides
Medallion Architecture
Bronze, Silver, Gold layers for modern data platforms
Data Mesh Guide
Decentralized data architecture patterns
Data Quality Best Practices
Validate and monitor data quality at scale
Data Contracts Guide
Schema, SLAs, and enforcement for reliable pipelines
Star Schema Generator
Generate a fact-and-dimensions diagram from one prompt
ER Diagram Generator
Draw tables and relationships from plain English or SQL