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:
2026-02-21 14:41:42 +03:00
co-authored by Claude Opus 4.6
parent 45f8f74378
commit 1d99539052
+67 -6
View File
@@ -291,7 +291,7 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а **выбор под задачу**. В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а **выбор под задачу**.
### 1. 3NF (третья нормальная форма) ### 1. 3NF (третья нормальная форма)
*Источник: Билл Инмон (Bill Inmon)* *Источник: Билл Инмон (Bill Inmon)*
Если упростить, 3NF - это когда данные о разных бизнес-сущностях хранятся в отдельных таблицах и связываются ключами: клиент, заказ, город, регион, страна и т.д. Вместо одной большой таблицы с большим числом дублирующихся данных мы получаем цепочку таблиц, связанных ключами: `Заказ → Клиент → Город → Регион → Страна`. JOIN-ов становится больше, зато одно и то же свойство (например, название города) хранится в одном месте, а не дублируется в каждой строке заказа. Если упростить, 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)* *Источник: Ральф Кимболл (Ralph Kimball)*
Самый распространённый способ построения таблиц для слоя витрин. Самый распространённый способ построения таблиц для слоя витрин. Именно эту модель мы использовали в [разделе 5](#5-базовые-понятия-факты-измерения-scd): схема `fact_sales` + `dim_date` / `dim_customer` / `dim_product` - это и есть Звезда.
Структура: Структура:
* в центре - таблица фактов (события и метрики); * в центре - таблица фактов (события и метрики);
* вокруг - измерения, обычно денормализованные («плоские») - широкие таблицы со всеми атрибутами сущности. Мы сознательно избегаем цепочек справочников ради простоты запросов. * вокруг - измерения, обычно денормализованные («плоские») - широкие таблицы со всеми атрибутами сущности. Мы сознательно избегаем цепочек справочников ради простоты запросов.
Пример: `fact_sales` + `dim_date`, `dim_customer`, `dim_product`.
![Звёздная схема вокруг fact_sales](images/star-model-small.jpg) ![Звёздная схема вокруг fact_sales](images/star-model-small.jpg)
Методология Ральфа Кимбалла (Ralph Kimball) как раз делает упор на такие звёздные схемы: витрины, которые максимально просты для чтения и понятны аналитикам и BI-инструментам. Методология Ральфа Кимбалла (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-инструментам удобно работать с такой моделью; * проста для понимания: аналитикам и BI-инструментам удобно работать с такой моделью;
@@ -343,7 +404,7 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
**Минусы:** **Минусы:**
* измерения денормализованы, поэтому атрибуты дублируются (например, регион повторяется у всех клиентов региона); * измерения денормализованы, поэтому атрибуты дублируются (например, название города повторяется у всех клиентов из этого города);
* изменения атрибутов могут требовать обновлять много строк в измерении. * изменения атрибутов могут требовать обновлять много строк в измерении.