# Star schema example (retail sales)

> A worked star schema example for retail sales analytics — a sales fact table at order-line grain with date, product, store, customer (SCD type 2) and promotion dimensions — with ER diagram and SQL DDL.

Source: https://entitymodel.com/docs/examples/star-schema/

A **star schema** is the classic design for analytics: one **fact table** of measurements in the middle, surrounded by **dimension tables** that describe them. This example is a retail sales star in the Kimball style, the kind of model that sits in a Snowflake, Databricks or BigQuery warehouse behind BI dashboards.

## Design notes

- **Declare the grain first.** `fact_sales` has one row per order line. Every measure (quantity, sales, cost, margin) is true at that grain and adds up across any dimension.
- **Surrogate keys everywhere.** Dimensions have integer `*_key` columns independent of source system IDs, which keep their natural keys (`sku`, `customer_id`) as ordinary attributes.
- **The date dimension is a table, not a function.** `dim_date` has a row per day with calendar and fiscal attributes, keyed `YYYYMMDD`.
- **Customer history is kept (SCD type 2).** When a customer's segment or city changes, a new row with new `valid_from`/`valid_to` dates is added; facts point at the version current at the time of sale.
- **No empty foreign keys.** Sales without a promotion point at a "No promotion" row (key 0), so joins never drop rows.
- **The order number is a degenerate dimension**: kept on the fact for drill-through, with no table of its own.
- **Dimensions are denormalized**: the product carries its category and subcategory directly, so reports need one join per dimension.

To extend it: add `fact_inventory_snapshot` (a periodic snapshot fact), `fact_returns`, or a `dim_employee` for the cashier. Model the normalized source system first; see [Conceptual, logical and physical data models](/docs/learn/conceptual-logical-physical-data-models/).

## Diagram

Retail Sales Star Schema: 6 entities, 0 reference lists and 5 relationships. Each box is an entity with its attributes (PK marks the primary key); solid lines are relationships in crow's foot notation, dashed lines point to reference lists.

<!-- diagram -->

**[Open this model in EntityModel.com](https://entitymodel.com/#/share/v1.rVpfb-O4Ef8qBIsCu4Cck-38s4s-OHGyDS6Jc2un17S3MBhpbLNLk1qSSk532Pc-96UfqN-kn6QgKcmirGTlrBcL7GoozT_O_Dgz9O94IeSaaDzEwDXV2VrEwEDiIF-YP4FUVHA87AZYiVRGgId4pXWihj_8UPnmIBJrHGBO1uYFpYmcq2gFa4IDrKlmhvoRNKEMTQkDhaaaSDQtXhFySTj9jWgrC19YxjcFYwS_knXCQOEAx6AiSZP8xRGK6Rq4UZEwZDVB74x05KS_RwshkXSClRVMOGGZppEaIpKTFiTSSJNHBohoJGQMssMoB7SUhHL0TPUKxURDgBIp4jTSAVJaSAhQlCot1iAR4bFZXAuj2EYpdYADXPoQdw9CHGCS6pWQ22biwDhOpwoPMUkSKZ4gxgGOJBAN8dxuUy_sHXe63U7Yn4Xh0P49CMPw7zjAaRK3es_uGgWFh__4vdgw44G5dYZxMVUJI9k8X3P79c688r74PJvrLMl3msdEWj0FE3IeidjQ_9C_OB6dXm5t2IQDkuIZJSCdo5Fx9BDpVeFtsbAPZhMP0A0QlUqzaxIQiWOq6RMgEkmhFIInkNnG18bVRGtJH1PtW2cNm3-GDAeYkUdgpVU_Wlpuy9nVh6vbGQ4wVfNE0jWRmf1oqGUKAZbwJaUS4uLZN2yaSimWRAP6DFlhhA0sKZ6NankM4yEehOZPt9fHX4NSR7N3NRXHhpunoVPvdUUm1qsuXIlCDw8PDzc347GvggmPbjf0NMhju6bEnaO20WPDvtsLu1XWNlvqG2Bou7E9PKoyLXKvxve8SMmdPTdbwSah86RFUSolcG2Aweyopmswu2tCynfpyUm_G9b86eBg26M5TOysYYj-jLjYAI2vgSfdJtecp-tHC-eFcBcdtwU5lz2dfby6_eAiP3-zTcyPYQkcpAnTMgtdKrujwmV44ACUixxixQJRrZB4rqk_nXQsam3nhoGIbVOuDULXLflmZFb5fkmJBbMK0582pJYce1WOKafaYEcEFZ73nGp0lxNzruOL86ub0TUOcCIhovkZ2wuwiohh23tV5OHBYOCBB1WRSLmek7X5p4oh-QoaFSv7UMCcJV5-W4DdEu4wdg-S_bArtg399z9o43D0v3_9GxWO8CPrcHAwOPWgQ6gGX50LtWc_9boHh56j1kQuKd8WfWPp-3dVdWOsgyqW106E04OjU_z1k6lBpDkGjArmFP0UYCKB4CG-JJFWftit5-ag2a4Zxo7aslY4GR2dn168WisYe83HKCZZDiju-C8XTAW2oMYxqKwCkIRESK3QgjJtKo5XioQdDuC31Aftz-FFyljh1kKTy5QxlDu1iI3R7KIFYqecfkmhITTLAtGvQrK5WMyfAT57fsjQZIF-dtT6mfFy-M9SUDHJvAQQXK9clFSi3xDRrSO2Z38rnsCifzWz83CYW0HV5C7i5CZfaHtgdBvZf0mJ1N5xVAr4qVxqW9Y0SsiANLJ_cPS2p1PYO_aCy2bIPAFJRVwNMJc5dwW9_S5cPvROOnehZwRVNoSAV0VcKRtBjliU3JPJ9cXo9lUBC8IU1LivBKMmsjzufymJu3N_DffGZTdXB7-8ZN7Gv7tyoSUEdi-P-8fHWxCY81EBWjCiNXCIHfjpFVCDihqWQmZoRUESGa0ypEQJehwgNmXXPwXlCj1CJni86a1eBMKd-4Dvbpd8TNxqHj6n1Zrix_sW4ekLuyU6lYTZzmwhxbpaoqpMaVj7GswuLjqTjx86N52z6_umFqmGX4VvXkCwb0H0RvDEjkEiNAMv4B8l8TLpLH-uC6pCo9SrZ8pjH1pctHioUpLaZ_woSYgE5u1R-tjAfepRdzg3OmpFpV9rKPpb1eVT9_iyB278eo8JD0zz55c_P2Mp-C2I2i7sr6l6S2G_Vci_DXxsU90wrsnJLYFnMDocdcMt4LFclC2qTLIIbudh0YpwDiyoYtASxFKSZJW9Mn_Zpf1vhyYvDQWcKGtcXda5I7YPw7vx32rlkeNey37H_btz_05IzYy_74BIL7lyr1fjt6TsBoROVSHz_az13zZwqnL91vjca4sbcqawoMpDwtKI3nD5WBDa78PPoLSfzSnXPoyVlPZcTVcOsZlDm2itTk4Sc8zOPa0nloYmfKv-rpZa3UEnPOqY0-ttGV0Mn7aT-nyz0jKvD0eH46PeVl4XjJSZDRJkuKAeUkw8s8ym95LyZXWaQxwVkBZIwXJtZmFCIhMbZiBrmHB4Nn1aYGqPFTU5kpU1SKrAAsgTYanBk-YpWjNsvGHAt6fJLVXF_O8bM75SRRo3aXg1_p5a5U-uTiHrcuKdK6V8rc7vp51ueNjr9xtVq8FVqdwbESuhtKEk4ARNKXsiHlq6cKlCZUlpn6fXIiNMZ-iDYPG-0WkXJGmBHE-E0XhuKswKx78aIrp0RB87Xm_bOmGv41fCjr8WW9xn4pu8_SgbDAaDTrfX6XftBZm2k2836G4M_M37tUYs_8pvxM5L4g6NmHn-jj7MDcQbO7FyqSV09i6Pxif9pl7McVKIJAmjEBtMdHeIeUVE0C_4tjKf_wXbAdY7Ayzhe4OP5a0QN-MrtCIKwTrRmcl39WpPtvtdwq6VVLdX63VykbVqaiNz54rqcnR93TtqlrLdU-VSvruyuiSMod7RH5FYLBqn5pZ1w8x85ui7AfgdyAi4DlA-dRUScVGvtfKX_NqSSF2f-5lbct08-KuP8sIaWACP69wueNyWV3-HIuaTcQmzd_dqRRO_9BcsL6XKXw94d82ayCVof5YcERlTTphB9CG-HXbdzxGeiYznpWMEy8sxaRJJQbnihpU4wI80phIibX8b0LxX9Xz7ZAUBXbo8qwyFa5cdc7FoZ9JmQtTGqskCbSZHdcOsyVd8T5ZVpzxV48x-Ed3OuKIDbb1ho0a7gDG1J6s2nWbdJntqtrCpUoO3Nssev3WzzkS6XOk92eWVwt59o7nlrR5_7WKyfL2NjfdGSIOFo_wUtObvKSYrp5zBlWKuSavwk_-Xi5zN1_8D)** to explore it, change it and export it. Nothing is uploaded: the model travels in the link. Or [download model.json](/docs/examples/star-schema/model.json).

## Entities

| Entity | Type | Description | Primary Key | Display Attribute | Attributes |
| --- | --- | --- | --- | --- | --- |
| Customer | Entity | Customers as a type 2 slowly changing dimension: a change to segment or city adds a new row, so history reports use the values at the time of sale. | customer_key | customer_name | 9 |
| Date | Entity | One row per calendar day, with every calendar and fiscal attribute reports filter on. | date_key | full_date | 10 |
| Product | Entity | Products, flattened with their category hierarchy so reports need no joins beyond the star. | product_key | product_name | 9 |
| Promotion | Entity | Promotions applied to a sale, with a "No promotion" row (key 0) so the fact never has empty keys. | promotion_key | promotion_name | 6 |
| Sales (fact) | Entity | One row per order line: the grain of the star. Measures are additive across every dimension. | sales_key | order_number | 14 |
| Store | Entity | Stores and the online channel, with their geography. | store_key | store_name | 8 |

## Attributes

### Customer

Customers as a type 2 slowly changing dimension: a change to segment or city adds a new row, so history reports use the values at the time of sale.

| Attribute | Data Type | Flags | Reference | Example | Description |
| --- | --- | --- | --- | --- | --- |
| customer_key | INT | PK, Required |  | 77310 | Surrogate key of this version. |
| customer_id | STRING | Required |  | CUS-104233 | Natural key; the same across versions. |
| customer_name | STRING | Label, Required, PII |  | Ana Silva |  |
| segment | STRING | Required |  | Loyalty Gold |  |
| city | STRING |  |  | Portland |  |
| country | STRING |  |  | United States |  |
| valid_from | DATE | Required |  | 2026-02-01 |  |
| valid_to | DATE | Required |  | 9999-12-31 | 9999-12-31 for the current version. |
| is_current | BOOLEAN | Required |  | true |  |

### Date

One row per calendar day, with every calendar and fiscal attribute reports filter on.

| Attribute | Data Type | Flags | Reference | Example | Description |
| --- | --- | --- | --- | --- | --- |
| date_key | INT | PK, Required |  | 20261103 | YYYYMMDD. |
| full_date | DATE | Label, Required |  | 2026-11-03 |  |
| day_of_week | STRING | Required |  | Tuesday |  |
| month_name | STRING | Required |  | November |  |
| calendar_month | INT | Required |  | 11 |  |
| calendar_quarter | INT | Required |  | 4 |  |
| calendar_year | INT | Required |  | 2026 |  |
| fiscal_period | STRING | Required |  | FY27-P04 |  |
| is_weekend | BOOLEAN | Required |  | false |  |
| is_holiday | BOOLEAN | Required |  | false |  |

### Product

Products, flattened with their category hierarchy so reports need no joins beyond the star.

| Attribute | Data Type | Flags | Reference | Example | Description |
| --- | --- | --- | --- | --- | --- |
| product_key | INT | PK, Required |  | 1201 | Surrogate key. |
| sku | STRING | Required |  | TEE-ORG-M-BLU | Natural key from the source system. |
| product_name | STRING | Label, Required |  | Organic Tee |  |
| brand | STRING |  |  | Northwind |  |
| category | STRING | Required |  | Apparel |  |
| subcategory | STRING | Required |  | T-shirts |  |
| size | STRING |  |  | M |  |
| color | STRING |  |  | Blue |  |
| list_price | DECIMAL(12,2) |  |  | 24.99 |  |

### Promotion

Promotions applied to a sale, with a "No promotion" row (key 0) so the fact never has empty keys.

| Attribute | Data Type | Flags | Reference | Example | Description |
| --- | --- | --- | --- | --- | --- |
| promotion_key | INT | PK, Required |  | 12 |  |
| promotion_code | STRING | Required |  | FALL25 |  |
| promotion_name | STRING | Label, Required |  | Fall 25% off |  |
| discount_type | STRING | Required |  | Percent | Percent, amount or none. |
| start_date | DATE |  |  | 2026-10-01 |  |
| end_date | DATE |  |  | 2026-10-31 |  |

### Sales (fact)

One row per order line: the grain of the star. Measures are additive across every dimension.

| Attribute | Data Type | Flags | Reference | Example | Description |
| --- | --- | --- | --- | --- | --- |
| sales_key | BIGINT | PK, Required |  | 900000123 | Surrogate key of the fact row. |
| date_key | INT | Required |  | 20261103 | Order date, as YYYYMMDD. |
| product_key | INT | Required |  | 1201 |  |
| store_key | INT | Required |  | 45 |  |
| customer_key | INT | Required |  | 77310 | The customer version current at the time of sale. |
| promotion_key | INT | Required |  | 0 | 0 = no promotion. |
| order_number | STRING | Label, Required |  | SO-2026-000123 | Degenerate dimension: the source order, with no table of its own. |
| line_number | INT | Required |  | 1 |  |
| quantity | INT | Required |  | 2 |  |
| unit_price | DECIMAL(12,2) | Required |  | 24.99 |  |
| discount_amount | DECIMAL(12,2) | Required |  | 0.00 |  |
| sales_amount | DECIMAL(12,2) | Required |  | 49.98 | quantity × unit_price − discount. |
| cost_amount | DECIMAL(12,2) | Required |  | 21.40 |  |
| margin_amount | DECIMAL(12,2) | Required |  | 28.58 | sales_amount − cost_amount. |

### Store

Stores and the online channel, with their geography.

| Attribute | Data Type | Flags | Reference | Example | Description |
| --- | --- | --- | --- | --- | --- |
| store_key | INT | PK, Required |  | 45 |  |
| store_code | STRING | Required |  | PDX-03 |  |
| store_name | STRING | Label, Required |  | Portland Pearl |  |
| channel | STRING | Required |  | Store | Store or online. |
| city | STRING |  |  | Portland |  |
| region | STRING | Required |  | West |  |
| country | STRING | Required |  | United States |  |
| opened_on | DATE |  |  | 2019-05-01 |  |

## Relationships

| Relationship | Source | Target | Cardinality | Forward Label | Reverse Label | Key | Description |
| --- | --- | --- | --- | --- | --- | --- | --- |
| sold_on | Sales (fact) | Date | N:1 | Sold On | Day Of | fact_sales.date_key |  |
| sale_of | Sales (fact) | Product | N:1 | Of Product | Sold In | fact_sales.product_key |  |
| sold_at | Sales (fact) | Store | N:1 | Sold At | Sells | fact_sales.store_key |  |
| sold_to | Sales (fact) | Customer | N:1 | Sold To | Bought | fact_sales.customer_key |  |
| under_promotion | Sales (fact) | Promotion | N:1 | Under | Applied To | fact_sales.promotion_key |  |

## SQL DDL (PostgreSQL)

Generated from the model. In the app, Export → Code also writes MySQL, Snowflake and Databricks DDL, DBML, dbt `schema.yml` and data contracts.

```sql
-- Retail Sales Star Schema — data model
-- PostgreSQL DDL, generated 2026-11-03
-- Made with EntityModel.com

CREATE TABLE fact_sales (
  sales_key BIGINT NOT NULL,
  date_key INTEGER NOT NULL,
  product_key INTEGER NOT NULL,
  store_key INTEGER NOT NULL,
  customer_key INTEGER NOT NULL,
  promotion_key INTEGER NOT NULL,
  order_number TEXT NOT NULL,
  line_number INTEGER NOT NULL,
  quantity INTEGER NOT NULL,
  unit_price NUMERIC(12,2) NOT NULL,
  discount_amount NUMERIC(12,2) NOT NULL,
  sales_amount NUMERIC(12,2) NOT NULL,
  cost_amount NUMERIC(12,2) NOT NULL,
  margin_amount NUMERIC(12,2) NOT NULL,
  PRIMARY KEY (sales_key)
);
COMMENT ON TABLE fact_sales IS 'One row per order line: the grain of the star. Measures are additive across every dimension.';
COMMENT ON COLUMN fact_sales.sales_key IS 'Surrogate key of the fact row.';
COMMENT ON COLUMN fact_sales.date_key IS 'Order date, as YYYYMMDD.';
COMMENT ON COLUMN fact_sales.customer_key IS 'The customer version current at the time of sale.';
COMMENT ON COLUMN fact_sales.promotion_key IS '0 = no promotion.';
COMMENT ON COLUMN fact_sales.order_number IS 'Degenerate dimension: the source order, with no table of its own.';
COMMENT ON COLUMN fact_sales.sales_amount IS 'quantity × unit_price − discount.';
COMMENT ON COLUMN fact_sales.margin_amount IS 'sales_amount − cost_amount.';

CREATE TABLE dim_date (
  date_key INTEGER NOT NULL,
  full_date DATE NOT NULL,
  day_of_week TEXT NOT NULL,
  month_name TEXT NOT NULL,
  calendar_month INTEGER NOT NULL,
  calendar_quarter INTEGER NOT NULL,
  calendar_year INTEGER NOT NULL,
  fiscal_period TEXT NOT NULL,
  is_weekend BOOLEAN NOT NULL,
  is_holiday BOOLEAN NOT NULL,
  PRIMARY KEY (date_key)
);
COMMENT ON TABLE dim_date IS 'One row per calendar day, with every calendar and fiscal attribute reports filter on.';
COMMENT ON COLUMN dim_date.date_key IS 'YYYYMMDD.';

CREATE TABLE dim_product (
  product_key INTEGER NOT NULL,
  sku TEXT NOT NULL,
  product_name TEXT NOT NULL,
  brand TEXT,
  category TEXT NOT NULL,
  subcategory TEXT NOT NULL,
  size TEXT,
  color TEXT,
  list_price NUMERIC(12,2),
  PRIMARY KEY (product_key)
);
COMMENT ON TABLE dim_product IS 'Products, flattened with their category hierarchy so reports need no joins beyond the star.';
COMMENT ON COLUMN dim_product.product_key IS 'Surrogate key.';
COMMENT ON COLUMN dim_product.sku IS 'Natural key from the source system.';

CREATE TABLE dim_store (
  store_key INTEGER NOT NULL,
  store_code TEXT NOT NULL,
  store_name TEXT NOT NULL,
  channel TEXT NOT NULL,
  city TEXT,
  region TEXT NOT NULL,
  country TEXT NOT NULL,
  opened_on DATE,
  PRIMARY KEY (store_key)
);
COMMENT ON TABLE dim_store IS 'Stores and the online channel, with their geography.';
COMMENT ON COLUMN dim_store.channel IS 'Store or online.';

CREATE TABLE dim_customer (
  customer_key INTEGER NOT NULL,
  customer_id TEXT NOT NULL,
  customer_name TEXT NOT NULL,
  segment TEXT NOT NULL,
  city TEXT,
  country TEXT,
  valid_from DATE NOT NULL,
  valid_to DATE NOT NULL,
  is_current BOOLEAN NOT NULL,
  PRIMARY KEY (customer_key)
);
COMMENT ON TABLE dim_customer IS 'Customers as a type 2 slowly changing dimension: a change to segment or city adds a new row, so history reports use the values at the time of sale.';
COMMENT ON COLUMN dim_customer.customer_key IS 'Surrogate key of this version.';
COMMENT ON COLUMN dim_customer.customer_id IS 'Natural key; the same across versions.';
COMMENT ON COLUMN dim_customer.valid_to IS '9999-12-31 for the current version.';

CREATE TABLE dim_promotion (
  promotion_key INTEGER NOT NULL,
  promotion_code TEXT NOT NULL,
  promotion_name TEXT NOT NULL,
  discount_type TEXT NOT NULL,
  start_date DATE,
  end_date DATE,
  PRIMARY KEY (promotion_key)
);
COMMENT ON TABLE dim_promotion IS 'Promotions applied to a sale, with a "No promotion" row (key 0) so the fact never has empty keys.';
COMMENT ON COLUMN dim_promotion.discount_type IS 'Percent, amount or none.';

-- Keys between tables
ALTER TABLE dim_date ADD CONSTRAINT uq_dim_date_full_date UNIQUE (full_date);
ALTER TABLE fact_sales ADD CONSTRAINT fk_sold_on FOREIGN KEY (date_key) REFERENCES dim_date (date_key);
ALTER TABLE fact_sales ADD CONSTRAINT fk_sale_of FOREIGN KEY (product_key) REFERENCES dim_product (product_key);
ALTER TABLE fact_sales ADD CONSTRAINT fk_sold_at FOREIGN KEY (store_key) REFERENCES dim_store (store_key);
ALTER TABLE fact_sales ADD CONSTRAINT fk_sold_to FOREIGN KEY (customer_key) REFERENCES dim_customer (customer_key);
ALTER TABLE fact_sales ADD CONSTRAINT fk_under_promotion FOREIGN KEY (promotion_key) REFERENCES dim_promotion (promotion_key);
```

## Mermaid

Paste into a `mermaid` code block on GitHub, GitLab or Notion to show the diagram there.

```mermaid
---
title: Retail Sales Star Schema
---
%% Made with EntityModel.com
erDiagram
  fact_sales {
    bigint sales_key PK "Surrogate key of the fact row."
    int date_key FK "Order date, as YYYYMMDD."
    int product_key FK "Product Key"
    int store_key FK "Store Key"
    int customer_key FK "The customer version current at the time of sale."
    int promotion_key FK "0 = no promotion."
    string order_number "Degenerate dimension: the source order, with no table of its own."
    int line_number "Line Number"
    int quantity "Quantity"
    decimal unit_price "Unit Price"
    decimal discount_amount "Discount Amount"
    decimal sales_amount "quantity × unit_price − discount."
    decimal cost_amount "Cost Amount"
    decimal margin_amount "sales_amount − cost_amount."
  }
  dim_date {
    int date_key PK "YYYYMMDD."
    date full_date "Full Date"
    string day_of_week "Day Of Week"
    string month_name "Month Name"
    int calendar_month "Calendar Month"
    int calendar_quarter "Calendar Quarter"
    int calendar_year "Calendar Year"
    string fiscal_period "Fiscal Period"
    boolean is_weekend "Is Weekend"
    boolean is_holiday "Is Holiday"
  }
  dim_product {
    int product_key PK "Surrogate key."
    string sku "Natural key from the source system."
    string product_name "Product Name"
    string brand "Brand"
    string category "Category"
    string subcategory "Subcategory"
    string size "Size"
    string color "Color"
    decimal list_price "List Price"
  }
  dim_store {
    int store_key PK "Store Key"
    string store_code "Store Code"
    string store_name "Store Name"
    string channel "Store or online."
    string city "City"
    string region "Region"
    string country "Country"
    date opened_on "Opened On"
  }
  dim_customer {
    int customer_key PK "Surrogate key of this version."
    string customer_id "Natural key; the same across versions."
    string customer_name "Customer Name"
    string segment "Segment"
    string city "City"
    string country "Country"
    date valid_from "Valid From"
    date valid_to "9999-12-31 for the current version."
    boolean is_current "Is Current"
  }
  dim_promotion {
    int promotion_key PK "Promotion Key"
    string promotion_code "Promotion Code"
    string promotion_name "Promotion Name"
    string discount_type "Percent, amount or none."
    date start_date "Start Date"
    date end_date "End Date"
  }
  fact_sales }o--|| dim_date : "Sold On"
  fact_sales }o--|| dim_product : "Of Product"
  fact_sales }o--|| dim_store : "Sold At"
  fact_sales }o--|| dim_customer : "Sold To"
  fact_sales }o--|| dim_promotion : "Under"
```

## Frequently asked questions

### What is a star schema?

A star schema is a dimensional data model with a central fact table holding numeric measurements at a declared grain, joined by foreign keys to denormalized dimension tables (date, product, customer, store) that describe the context. Drawn as a diagram, the dimensions surround the fact like the points of a star.

### What is the difference between a star schema and a snowflake schema?

In a star schema each dimension is a single denormalized table. In a snowflake schema dimensions are normalized into several tables (product, then subcategory, then category). Stars are simpler and faster to query; snowflakes save some storage and enforce hierarchies.

### What is a slowly changing dimension (SCD) type 2?

A way to keep history in a dimension: instead of overwriting a changed attribute, add a new row with its own surrogate key and validity dates, and mark it current. Facts keep pointing at the version that was true when they happened.
