A dimension table has one row per thing and as many descriptive columns as analysts might want, spelled out as words: a region rather than a list of country codes, "short" rather than a page range. Each gets a surrogate key, a meaningless integer the warehouse owns, beside the app's natural key. Surrogate keys keep facts narrow, survive a change of source system, and allow several versions of one customer (SCD Type 2).
DROP TABLE IF EXISTS mart.fact_sales, mart.dim_book, mart.dim_customer, mart.dim_order_profile;
CREATE TABLE mart.dim_book (
book_key int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- surrogate key
book_id int NOT NULL UNIQUE, title text, author text, -- natural key
genre text, genre_group text, list_price numeric(6,2), length_band text);
INSERT INTO mart.dim_book (book_id, title, author, genre, genre_group, list_price, length_band)
SELECT b.book_id, b.title, b.author, b.genre, g.parent, b.price,
CASE WHEN b.pages < 250 THEN 'short' WHEN b.pages < 400 THEN 'medium' ELSE 'long' END
FROM books b JOIN mart.genre_tree g ON g.node = b.genre;
CREATE TABLE mart.dim_customer (
customer_key int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id int NOT NULL UNIQUE, name text, country char(2), region text, signup_date date);
INSERT INTO mart.dim_customer (customer_id, name, country, region, signup_date)
SELECT customer_id, name, country, CASE WHEN country IN ('US', 'CA') THEN 'Americas'
WHEN country IN ('GB', 'DE') THEN 'Europe' ELSE 'Asia-Pacific' END, signup_date
FROM customers ORDER BY customer_id;
CREATE TABLE mart.dim_order_profile ( -- junk dimension: low-cardinality flags
profile_key int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
channel text, status text, coupon text, UNIQUE (channel, status, coupon));
INSERT INTO mart.dim_order_profile (channel, status, coupon)
SELECT DISTINCT channel, status, coalesce(coupon, 'none') FROM orders ORDER BY 1, 2, 3;
SELECT book_key, book_id, genre, genre_group, length_band FROM mart.dim_book ORDER BY 1; book_key | book_id | genre | genre_group | length_band
----------+---------+-----------------+---------------+-------------
1 | 1 | Fiction | Fiction & Lit | medium
2 | 5 | Science Fiction | Fiction & Lit | medium
3 | 3 | Cooking | Practical | medium
...
6 | 4 | Travel | Nonfiction | shortSurrogate keys follow load order, not book_id, so nothing may depend on their values. dim_order_profile is a junk dimension: channel, status and coupon would each make a tiny dimension, so one table holds the 48 combinations that occur. The order number stays in the fact table as a degenerate dimension, a key with no table behind it. The date dimension comes from A Date Dimension.