Что такое Star Schema
Star Schema (звёздная схема) — самая распространённая dimensional model. Структура:
- Один fact table в центре.
- Несколько dimensions вокруг, соединённые с fact через foreign keys.
- Dimensions денормализованы — иерархия и связанные атрибуты inline.
Визуально похоже на звезду: fact в центре, dimensions — лучи.
dim_date
|
|
dim_customer --- fact_orders --- dim_product
|
|
dim_store
Простая структура, простой SQL, быстрые queries. Это стандарт BI tools (Tableau, Looker, Power BI) — они оптимизированы под star schema.
Пример: e-commerce sales mart
Возьмём интернет-магазин. Бизнес-процесс: заказ был размещён. Грейн: один товар в заказе (line item).
Fact table
CREATE TABLE marts.sales.fact_order_items (
-- Surrogate PK
order_item_key BIGINT PRIMARY KEY,
-- Foreign keys к dimensions
date_key INT, -- когда заказ
customer_key BIGINT, -- кто
product_key BIGINT, -- что
store_key BIGINT, -- где (channel/online store)
promotion_key BIGINT, -- по какой промо (NULL если без)
-- Degenerate dimension (см. ниже)
order_id BIGINT NOT NULL,
-- Numeric measures (additive)
qty INT NOT NULL,
unit_price NUMERIC(10, 2),
list_price NUMERIC(10, 2),
total_amount NUMERIC(12, 2), -- qty * unit_price
discount_amount NUMERIC(10, 2),
net_amount NUMERIC(12, 2), -- total - discount
cost_amount NUMERIC(12, 2),
margin_amount NUMERIC(12, 2) -- net - cost
);
Грейн — один line item (один товар в заказе). Если в заказе 3 товара — 3 строки fact. Это даёт максимальную гибкость для analytics.
Dimensions
dim_date — calendar:
CREATE TABLE marts.sales.dim_date (
date_key INT PRIMARY KEY, -- 20260517 (yyyymmdd format)
full_date DATE,
year INT,
quarter INT,
quarter_name TEXT, -- 'Q1', 'Q2'
month INT,
month_name TEXT, -- 'January'
week_of_year INT,
day_of_month INT,
day_of_week INT,
day_of_week_name TEXT, -- 'Monday'
is_weekend BOOLEAN,
is_holiday BOOLEAN,
holiday_name TEXT,
fiscal_year INT,
fiscal_quarter INT
);
Не загружается из источника — генерируется в DWH на 10-20 лет вперёд. Используется всеми marts.
dim_customer — клиенты:
CREATE TABLE marts.sales.dim_customer (
customer_key BIGINT PRIMARY KEY,
customer_id BIGINT, -- natural key
email TEXT,
name TEXT,
country TEXT,
city TEXT,
-- Сегментация
customer_segment TEXT, -- 'premium', 'standard', 'churned'
age_band TEXT, -- '18-24', '25-34', '35-44', '45-54', '55+'
signup_date DATE,
tenure_years INT, -- pre-computed
-- Demographics
preferred_language TEXT,
marketing_consent BOOLEAN,
-- SCD Type 2
valid_from DATE,
valid_to DATE,
is_current BOOLEAN
);
dim_product — товары с денормализованной иерархией:
CREATE TABLE marts.sales.dim_product (
product_key BIGINT PRIMARY KEY,
product_id BIGINT, -- natural key (SKU)
product_name TEXT,
product_description TEXT,
-- Иерархия inline (денормализация!)
category_name TEXT, -- 'Electronics'
subcategory_name TEXT, -- 'Smartphones'
department_name TEXT, -- 'Consumer Electronics'
-- Brand info inline
brand_name TEXT,
brand_country TEXT,
brand_segment TEXT, -- 'luxury', 'mainstream', 'budget'
-- Pricing tier
list_price NUMERIC(10, 2),
price_band TEXT, -- 'low', 'medium', 'premium'
-- Physical attributes
weight_kg NUMERIC(8, 2),
is_seasonal BOOLEAN,
-- SCD Type 2
valid_from DATE,
valid_to DATE,
is_current BOOLEAN
);
Обратите внимание: category_name, subcategory_name, department_name — все inline. Не отдельная dim_category таблица. Это и есть денормализация Kimball — единственная dimension содержит всю иерархию.
dim_store — каналы продаж:
CREATE TABLE marts.sales.dim_store (
store_key BIGINT PRIMARY KEY,
store_id BIGINT,
store_name TEXT,
channel TEXT, -- 'web', 'mobile_app', 'pos', 'marketplace'
region TEXT,
country TEXT,
is_active BOOLEAN
);
dim_promotion:
CREATE TABLE marts.sales.dim_promotion (
promotion_key BIGINT PRIMARY KEY,
promotion_id BIGINT,
promotion_name TEXT,
campaign_name TEXT,
promo_type TEXT, -- 'percentage', 'bogo', 'fixed'
discount_value NUMERIC(10, 2),
start_date DATE,
end_date DATE
);
Visual
Простая структура, легко читается, быстрые JOIN-ы
Degenerate dimension
Заметили, что в fact_order_items есть order_id, но нет dim_order? Это degenerate dimension.
Order_id — это идентификатор заказа в OLTP. Все его атрибуты (customer, date, etc) уже разнесены по dimensions через FK. Поэтому dim_order не нужна — её attributes уже есть в других dimensions. Но order_id полезен:
- Группировка line items одного заказа:
GROUP BY order_id. - Drill-back в OLTP-систему: “найди заказ 12345 в Postgres”.
Решение — хранить order_id прямо в fact без отдельной dimension. Это degenerate dimension (или junk dimension в другой терминологии).
Query на star schema
Типичный BI запрос:
SELECT
d.year,
d.month_name,
p.category_name,
c.country,
SUM(f.net_amount) AS revenue,
SUM(f.margin_amount) AS margin,
SUM(f.qty) AS units_sold,
COUNT(DISTINCT f.order_id) AS orders_count,
COUNT(DISTINCT f.customer_key) AS unique_customers
FROM marts.sales.fact_order_items f
JOIN marts.sales.dim_date d ON f.date_key = d.date_key
JOIN marts.sales.dim_customer c ON f.customer_key = c.customer_key
JOIN marts.sales.dim_product p ON f.product_key = p.product_key
WHERE d.year = 2026
AND c.country IN ('US', 'CA', 'MX')
GROUP BY 1, 2, 3, 4
ORDER BY revenue DESC;
Чистый, читаемый SQL. Три JOIN, чёткая структура. На columnar storage с partitioning по date_key — секунды на миллиардах строк.
Почему star — стандарт BI
1. Простота для аналитика. SQL пишется естественно: SELECT measures FROM fact JOIN dims. Минимум “thinking time” для понимания структуры.
2. Производительность. Минимум JOIN-ов (всё через fact). Денормализованные dimensions устраняют дополнительные JOIN-ы в иерархиях. Современные columnar DWH идеально подходят.
3. BI tools fit. Tableau, Looker, Power BI generated SQL автоматически из drag-and-drop UI работает на star schemas из коробки. Snowflake schemas (нормализованные dims) хуже работают.
4. Симметрия. Каждая dimension — equal citizen. Можно делать slicing по любому atribute любой dimension без особого мышления о joins.
5. Кеширование. BI tools кэшируют join result. Star — простой shape для кэширования.
Conformed dimensions: для multiple marts
Если у вас несколько marts (sales, marketing, finance), и в каждом есть dim_customer — они должны быть conformed: одни и те же customer_keys, одни атрибуты, одна логика. Иначе marts будут противоречить друг другу.
Решение: один dim_customer в core слое, marts используют его через JOIN или копируют. dbt автоматизирует это через model references.
# Реальная dbt-структура
models/
core/
dim_customer.sql # conformed, single source of truth
dim_product.sql
dim_date.sql
marts/
sales/
fact_order_items.sql # references core dimensions
marketing/
fact_email_events.sql # references same dim_customer
finance/
fact_payments.sql # references same dim_customer
Это даёт data bus (Kimball term): marts соединены через shared conformed dimensions. Sales analytics и marketing analytics видят одинаковых customers, можно делать cross-mart queries.
Conformed dimensions — главное правило при scaling Kimball-подхода. Без них marts расходятся, появляются inconsistencies, теряется trust в данных. dbt сильно помогает соблюдать conformity через model references и testing.
Pre-computed measures
В fact-таблицу можно (и часто стоит) добавить pre-computed measures:
total_amount = qty * unit_price— посчитано в ETL, не на лету.net_amount = total_amount - discount_amount.margin_amount = net_amount - cost_amount.
Это денормализация measures. Стоимость — дублирование данных (можно посчитать на лету), но преимущество — простой SQL для аналитика и быстрые queries.
-- С pre-computed measures: чистый SQL
SELECT SUM(margin_amount) FROM fact_order_items;
-- Без: каждый аналитик вычисляет на лету, легко ошибиться
SELECT SUM(qty * unit_price - discount_amount - cost_amount) FROM fact_order_items;
Pre-computed measures — стандарт Kimball. Принцип: make BI easy for analysts. Они не должны помнить бизнес-формулы — формулы embedded в fact.
Когда star не подходит
Star schema — отличный default, но не панацея. Где он плохо работает:
1. Many-to-many relationships. Если один заказ может относиться к multiple campaigns (a customer responded to email AND saw a Facebook ad before buying), star становится сложным. Решение — bridge tables.
2. Hierarchies с многими уровнями. Если категория имеет 7 уровней — денормализация раздувает dim_product. Тогда snowflake (lesson 3) или recursive structure.
3. Slowly changing attributes с большим объёмом изменений. Если customer.country меняется каждый месяц — SCD Type 2 раздувает dim_customer быстро. Тогда compromise (Type 3 или history bridge).
4. Streaming аналитика с sub-second latency. Star schema хорош для batch-аналитики. Для real-time часто используют denormalized OBT (One Big Table) — fact с inline dim attributes.
Star vs OBT
OBT (One Big Table) — крайний случай денормализации. Всё в одной таблице:
CREATE TABLE marts.orders_obt (
-- Все атрибуты заказа
order_id BIGINT,
date DATE,
-- Атрибуты клиента (inline)
customer_id BIGINT,
customer_name TEXT,
customer_country TEXT,
customer_segment TEXT,
-- Атрибуты продукта (inline)
product_id BIGINT,
product_name TEXT,
category_name TEXT,
brand_name TEXT,
-- Measures
qty INT,
amount NUMERIC,
-- ... 50 колонок
);
Никаких JOIN-ов. Все queries — flat scans. На ClickHouse/columnar DBs часто самый быстрый вариант. Цена — дублирование (customer name в каждой строке), сложность maintenance (изменение customer.country = обновление миллионов orders).
Star vs OBT — это trade-off:
- Star: меньше storage, проще updates, чуть медленнее queries.
- OBT: больше storage, сложнее updates, быстрее queries.
Для batch DWH (Snowflake/BigQuery) — обычно star. Для real-time OLAP (ClickHouse) — часто OBT.
Snowflake schema — нормализованная вариация
У star есть близкий родственник — snowflake schema. Это та же звезда, но dimensions частично нормализованы: вместо одной wide dim_product с inline иерархией (category_name, subcategory_name, department_name колонками внутри) — отдельные таблицы dim_category, dim_subcategory, dim_department, связанные FK-цепочкой. Получается snowflake (снежинка): fact в центре, dimensions ветвятся в подtaблицы.
Плюсы нормализации: меньше storage на больших dimensions, чистота при rename категорий, явная иерархия в схеме. Минусы существенные: больше JOIN-ов в каждом запросе (для запроса по department нужно JOIN fact -> dim_product -> dim_subcategory -> dim_department вместо одного JOIN до dim_product); BI tools (Tableau, Looker) хуже работают с snowflake, оптимизированы под star; SQL аналитика становится сложнее.
В 2026 году snowflake schema редко используется в чистом виде. На columnar DWH (Snowflake, BigQuery) dictionary encoding делает дублирование значений почти бесплатным по storage — миллион Electronics в dim_product занимает несколько байт. Главный исторический аргумент за нормализацию пропадает.
Snowflake встречается в legacy DWH банков и страховых (регуляторика требует нормализации), в healthcare DWH (ICD-10 codes имеют семиуровневую иерархию), в редких retail-системах Amazon-масштаба с миллиардами продуктов. В стартапах и SMB-проектах — star везде.
Часто встречается гибрид: большая часть dimensions денормализованы (star), но некоторые специфические — нормализованы. Например, dim_customer и dim_product денормализованы, а dim_geography (страны -> регионы -> города) — нормализована, потому что используется в нескольких dimensions. Это нормально — нет догмы “всё star или всё snowflake”.
Не путайте snowflake schema (методология dimensional modeling) с Snowflake (cloud DWH компания). Это совершенно разные вещи, просто одинаковое имя.
Дизайн dimensional моделей разбираем в будущем dwh-design-course.
Попробуй сам
- Спроектируй sales mart для своего hypothetical e-commerce: какие fact, dimensions, measures? Реализуй CREATE TABLE statements.
- На public dataset (например, TPC-H lineitem, orders, customer, supplier, part) — построй star schema. Сравни SQL запросы на 3NF (TPC-H native) vs на star (твой mart).
- Найди в открытом dbt project (dbt-jaffle-shop) marts/. Идентифицируй fact и dimensions. Зарисуй star schema diagram.
- Спроектируй marketing mart с conformed dim_customer (shared с sales mart). Какой будет fact для email events?