docs(modeling): расширены разделы 3NF и Звезда практическими примерами
- Зачем:
- разделы 3NF и Звезда были слишком краткими для учебного материала,
менти не видел разницу между моделями на практике
- Что:
- 3NF: добавлена mermaid-диаграмма с dim_city и SQL-запрос (3 JOIN)
- Звезда: добавлена явная связь с разделом 5, SQL-запрос (2 JOIN) для контраста
- оба примера отвечают на один вопрос: «сколько потратил клиент из Москвы?»
- Проверка:
- визуальная проверка diff
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+67
-6
@@ -291,7 +291,7 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
|
||||
|
||||
В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а **выбор под задачу**.
|
||||
|
||||
### 1. 3NF (третья нормальная форма)
|
||||
### 1. 3NF (третья нормальная форма)
|
||||
*Источник: Билл Инмон (Bill Inmon)*
|
||||
|
||||
Если упростить, 3NF - это когда данные о разных бизнес-сущностях хранятся в отдельных таблицах и связываются ключами: клиент, заказ, город, регион, страна и т.д. Вместо одной большой таблицы с большим числом дублирующихся данных мы получаем цепочку таблиц, связанных ключами: `Заказ → Клиент → Город → Регион → Страна`. JOIN-ов становится больше, зато одно и то же свойство (например, название города) хранится в одном месте, а не дублируется в каждой строке заказа.
|
||||
@@ -303,6 +303,53 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
|
||||
* Данные о сущностях разнесены по отдельным таблицам: клиент, заказ, продукт.
|
||||
* Минимум дублирования: общие атрибуты хранятся в одном месте, таблицы связаны ключами.
|
||||
|
||||
#### Как выглядел бы наш магазин в 3NF
|
||||
|
||||
В нашем примере `city` лежит прямо в `dim_customer`. В 3NF город стал бы отдельной таблицей, чтобы название хранилось в одном месте:
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
dim_city ||--o{ dim_customer : "город"
|
||||
dim_customer ||--o{ fact_sales : "клиент"
|
||||
|
||||
dim_city {
|
||||
int city_id PK
|
||||
varchar city_name "Москва, СПб, ..."
|
||||
}
|
||||
|
||||
dim_customer {
|
||||
bigint customer_sk PK
|
||||
int customer_bk
|
||||
varchar email
|
||||
int city_id FK "ссылка на dim_city"
|
||||
date valid_from
|
||||
date valid_to
|
||||
}
|
||||
|
||||
fact_sales {
|
||||
bigint sale_id PK
|
||||
bigint customer_sk FK
|
||||
int date_key FK
|
||||
int quantity
|
||||
decimal amount
|
||||
}
|
||||
```
|
||||
|
||||
Теперь, чтобы узнать **«сколько потратил клиент из Москвы за январь 2024?»**, нужно пройти по цепочке:
|
||||
|
||||
```sql
|
||||
-- 3NF: три JOIN, чтобы добраться до города
|
||||
SELECT SUM(f.amount)
|
||||
FROM dds.fact_sales f
|
||||
JOIN dds.dim_customer c ON f.customer_sk = c.customer_sk
|
||||
JOIN dds.dim_city ct ON c.city_id = ct.city_id
|
||||
JOIN dds.dim_date d ON f.date_key = d.date_key
|
||||
WHERE ct.city_name = 'Москва'
|
||||
AND d.year = 2024 AND d.month = 1;
|
||||
```
|
||||
|
||||
Запрос читаемый, но JOIN-ов уже три - и это для простого вопроса. В реальном ядре цепочка может быть длиннее: `Клиент → Город → Регион → Страна`.
|
||||
|
||||
**Плюсы:**
|
||||
|
||||
* Удобно поддерживать **единую «карту бизнеса»**: где живут «клиент», «заказ», «договор» и как они связаны;
|
||||
@@ -319,22 +366,36 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
|
||||
|
||||
---
|
||||
|
||||
### 2. Звезда (Star Schema)
|
||||
### 2. Звезда (Star Schema)
|
||||
*Источник: Ральф Кимболл (Ralph Kimball)*
|
||||
|
||||
Самый распространённый способ построения таблиц для слоя витрин.
|
||||
Самый распространённый способ построения таблиц для слоя витрин. Именно эту модель мы использовали в [разделе 5](#5-базовые-понятия-факты-измерения-scd): схема `fact_sales` + `dim_date` / `dim_customer` / `dim_product` - это и есть Звезда.
|
||||
|
||||
Структура:
|
||||
|
||||
* в центре - таблица фактов (события и метрики);
|
||||
* вокруг - измерения, обычно денормализованные («плоские») - широкие таблицы со всеми атрибутами сущности. Мы сознательно избегаем цепочек справочников ради простоты запросов.
|
||||
|
||||
Пример: `fact_sales` + `dim_date`, `dim_customer`, `dim_product`.
|
||||
|
||||

|
||||
|
||||
Методология Ральфа Кимбалла (Ralph Kimball) как раз делает упор на такие звёздные схемы: витрины, которые максимально просты для чтения и понятны аналитикам и BI-инструментам.
|
||||
|
||||
#### Тот же вопрос - в Звезде
|
||||
|
||||
В Звезде `city` лежит прямо в `dim_customer` (денормализовано). Тот же отчёт выглядит проще:
|
||||
|
||||
```sql
|
||||
-- Звезда: два JOIN, город - прямо в измерении
|
||||
SELECT SUM(f.amount)
|
||||
FROM dds.fact_sales f
|
||||
JOIN dds.dim_customer c ON f.customer_sk = c.customer_sk
|
||||
JOIN dds.dim_date d ON f.date_key = d.date_key
|
||||
WHERE c.city = 'Москва'
|
||||
AND d.year = 2024 AND d.month = 1;
|
||||
```
|
||||
|
||||
На один JOIN меньше, и не нужно знать, где именно хранится город: он лежит прямо в карточке клиента. Для аналитика или BI-инструмента это большая разница.
|
||||
|
||||
**Плюсы:**
|
||||
|
||||
* проста для понимания: аналитикам и BI-инструментам удобно работать с такой моделью;
|
||||
@@ -343,7 +404,7 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
|
||||
|
||||
**Минусы:**
|
||||
|
||||
* измерения денормализованы, поэтому атрибуты дублируются (например, регион повторяется у всех клиентов региона);
|
||||
* измерения денормализованы, поэтому атрибуты дублируются (например, название города повторяется у всех клиентов из этого города);
|
||||
* изменения атрибутов могут требовать обновлять много строк в измерении.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user