docs(review): ADR-1, ADR-2, ADR-3 — архитектурные решения
- Зачем:
- зафиксировать принятые архитектурные решения с обоснованием для менти,
чтобы студенты понимали границы учебных упрощений и реальную практику.
- Что:
- ADR-1: города/страны/модели — атрибуты измерений (star vs snowflake).
- ADR-2: партиционирование не используем (учебные объёмы), с примером
exchange partition и контекстом импортозамещения (Teradata/Exadata → GP).
- ADR-3: явный storage type для каждой таблицы — AO Column для write-once,
AO Row для snapshot-справочников (TRUNCATE+INSERT), heap для UPDATE-таблиц.
- добавлена задача P2 по реализации ADR-3, обновлена сводка трудозатрат.
- Проверка:
- cat docs/internal/architecture_review.md | grep "ADR-".
This commit is contained in:
@@ -97,6 +97,23 @@ GP-специфичная best practice, которую забывают даж
|
||||
|
||||
### P2: Средние усилия, заметное улучшение качества
|
||||
|
||||
- [ ] **Явный storage type для всех таблиц + AO где возможно** ✅ РЕШЕНИЕ ПРИНЯТО
|
||||
- 18 из 28 таблиц имеют неявный heap (нет `WITH`) — студент не видит, что выбор сделан
|
||||
- **Целевая раскладка по storage:**
|
||||
- **AO Column Store**: `dds.dim_calendar` (write-once, generate_series)
|
||||
- **AO Row + zlib**: ODS snapshot-справочники (`airports`, `airplanes`, `routes`, `seats`)
|
||||
— перевести загрузку с UPSERT на TRUNCATE+INSERT (честнее для full snapshot семантики)
|
||||
- **AO Row + zlib**: `dds.dim_tariffs` (только INSERT, нет UPDATE)
|
||||
- **AO Row + zlib**: `dm.route_performance` (full rebuild, по дизайну)
|
||||
- **Heap (явный)**: ODS транзакционные (`bookings`, `tickets`, `flights`, `segments`,
|
||||
`boarding_passes`) — row-level UPDATE при SCD1 UPSERT
|
||||
- **Heap (явный)**: DDS измерения с UPDATE (`dim_airports`, `dim_airplanes`,
|
||||
`dim_passengers`, `dim_routes`) и `fact_flight_sales`
|
||||
- **Heap (явный)**: DM витрины с UPSERT (`sales_report` и будущие HWM-витрины)
|
||||
- К каждой таблице добавить комментарий, объясняющий выбор storage type
|
||||
- Файлы: все `*_ddl.sql` в ods/, dds/, dm/ + переписать 4 ODS snapshot load-скрипта
|
||||
- См. ADR-3
|
||||
|
||||
- [ ] **Дублирование hashdiff CTE в dim_routes_load.sql**
|
||||
- `md5(COALESCE(...))` повторяется в Statement 1 и Statement 2, ROW_NUMBER() — 3 раза
|
||||
- **Решение**: вынести в `CREATE TEMP TABLE tmp_routes_src ON COMMIT DROP`
|
||||
@@ -137,6 +154,128 @@ GP-специфичная best practice, которую забывают даж
|
||||
|
||||
---
|
||||
|
||||
## ПРИНЯТЫЕ АРХИТЕКТУРНЫЕ РЕШЕНИЯ
|
||||
|
||||
### ADR-1: Города, страны, модели самолётов — атрибуты измерений, не отдельные справочники
|
||||
|
||||
**Рассматривалось**: выделить `dim_city`, `dim_country`, `dim_airplane_model` как отдельные
|
||||
измерения со своими суррогатными ключами.
|
||||
|
||||
**Решение**: оставить `city`, `country` как атрибуты `dim_airports`, а `model` — как атрибут
|
||||
`dim_airplanes`. Не создавать отдельные справочники.
|
||||
|
||||
**Обоснование**:
|
||||
1. **Star vs Snowflake.** Kimball-методология рекомендует «wide and flat» измерения.
|
||||
Вынос атрибутов в подтаблицы превращает star schema в snowflake — добавляет 2-3 JOIN-а
|
||||
в каждый запрос к факту без аналитического выигрыша. Для учебного стенда star schema —
|
||||
правильный эталон.
|
||||
2. **Нет самостоятельной сущности в домене.** Город — JSON-атрибут аэропорта в источнике
|
||||
(`airport_name::json->>'ru'`). У него нет своего бизнес-ключа, жизненного цикла,
|
||||
независимых атрибутов. Модель самолёта — аналогично.
|
||||
3. **Когнитивная нагрузка.** Лестница ODS→DDS уже крутая (6 измерений + 1 факт + SCD2).
|
||||
Добавление 2-3 измерений усложнит стенд без пропорционального обучающего эффекта.
|
||||
|
||||
**Когда отдельное измерение оправдано** (для справки студентам):
|
||||
- Город имеет собственные атрибуты из другого источника (население, регион, координаты)
|
||||
→ `dim_geography` как outrigger-измерение
|
||||
- Модель самолёта имеет независимые характеристики (производитель, сертификация, конфигурации)
|
||||
→ `dim_aircraft_type`
|
||||
- В Data Vault — `hub_city` / `hub_country` как самостоятельные бизнес-объекты (другая парадигма)
|
||||
|
||||
### ADR-2: Heap + UPSERT вместо AO + партиционирование + exchange partition
|
||||
|
||||
**Контекст**: Greenplum широко распространён в РФ — на него активно мигрировали при
|
||||
импортозамещении с Teradata и Exadata. Именно поэтому GP выбран для курсовой: опыт работы
|
||||
с ним будет напрямую релевантен первой работе студента. Тем важнее, чтобы студенты понимали,
|
||||
как устроены реальные GP-хранилища, даже если стенд использует упрощённый подход.
|
||||
|
||||
**Рассматривалось**: использовать production-паттерн крупных GP-хранилищ:
|
||||
- AO Column Store (сжатие zlib/zstd, векторное чтение, колоночное хранение)
|
||||
- Range-партиционирование по дате (`PARTITION BY RANGE (flight_date)`)
|
||||
- Обновление через замену партиций (`ALTER TABLE EXCHANGE PARTITION`) или
|
||||
`DELETE + INSERT` в рамках одной партиции вместо row-level UPDATE
|
||||
|
||||
**Решение**: партиционирование не применяем (учебные объёмы). Для storage —
|
||||
дифференцированный подход: heap для таблиц с UPDATE, AO для иммутабельных
|
||||
(см. ADR-3 с полной раскладкой).
|
||||
|
||||
**Обоснование**:
|
||||
1. **Универсальность паттерна.** UPSERT через UPDATE + INSERT работает в PostgreSQL,
|
||||
Snowflake, BigQuery, Redshift — везде. Exchange partition — GP-специфика
|
||||
(`ALTER TABLE ... EXCHANGE PARTITION FOR (...) WITH TABLE tmp_...`).
|
||||
Студент, освоив UPSERT, сможет применить его на любой платформе.
|
||||
2. **Объём данных.** На учебных ~100K строк партиционирование не даёт partition pruning
|
||||
эффекта, зато утраивает DDL (стратегия, sub-partitions, retention policy).
|
||||
Выигрыш нулевой, когнитивная нагрузка — существенная.
|
||||
3. **Простота ментальной модели.** «Вот строка, она обновилась» понятнее, чем «вот партиция,
|
||||
она заменилась целиком». Второй паттерн требует понимания storage engine, что выходит
|
||||
за рамки первого курса DWH.
|
||||
|
||||
**Что студенту важно знать про реальный GP** (для менти):
|
||||
|
||||
На продакшн-хранилищах с десятками и сотнями миллионов строк подход меняется принципиально:
|
||||
|
||||
| Аспект | Стенд (учебный) | Продакшн (реальный GP) |
|
||||
|--------|-----------------|----------------------|
|
||||
| Хранение фактов | Heap (row-oriented) | AO Column Store (сжатие, колонки) |
|
||||
| Партиционирование | Нет | Range по дате (день/месяц) |
|
||||
| Обновление | Row-level UPDATE | Exchange partition или DELETE+INSERT в партиции |
|
||||
| Причина | UPDATE на AO «раздувает» таблицу (помечает строки deleted, дописывает новые) | |
|
||||
| Когда переходить | > 10M строк, или когда VACUUM не справляется | |
|
||||
|
||||
Типичный production-паттерн загрузки факта по дням:
|
||||
```sql
|
||||
-- 1. Собрать новую партицию во временную таблицу
|
||||
CREATE TABLE tmp_fact_20170102 (LIKE dds.fact_flight_sales)
|
||||
WITH (appendonly=true, orientation=column, compresstype=zlib);
|
||||
INSERT INTO tmp_fact_20170102 SELECT ... FROM ods... WHERE flight_date = '2017-01-02';
|
||||
|
||||
-- 2. Атомарно заменить партицию (без DELETE, без UPDATE)
|
||||
ALTER TABLE dds.fact_flight_sales
|
||||
EXCHANGE PARTITION FOR ('2017-01-02') WITH TABLE tmp_fact_20170102;
|
||||
|
||||
-- 3. Удалить временную таблицу (теперь в ней старые данные)
|
||||
DROP TABLE tmp_fact_20170102;
|
||||
```
|
||||
|
||||
Преимущества exchange partition:
|
||||
- Нет row-level UPDATE → нет bloat, не нужен VACUUM
|
||||
- AO Column Store даёт 5-10x сжатие и быстрые аналитические скана
|
||||
- Partition pruning: запрос `WHERE flight_date = '2017-01-02'` читает только одну партицию
|
||||
- Атомарность: EXCHANGE — одна DDL-команда, нет окна неконсистентности
|
||||
|
||||
### ADR-3: Явный storage type для каждой таблицы + AO где нет UPDATE
|
||||
|
||||
**Проблема**: 18 из 28 таблиц в ODS/DDS/DM создаются без `WITH`-клаузы. GP по умолчанию
|
||||
создаёт heap, но студент не видит осознанного выбора — таблица «просто создаётся».
|
||||
В учебном стенде каждое решение должно быть видимым и объяснённым.
|
||||
|
||||
**Решение**: добавить явный `WITH (...)` ко всем таблицам. Где row-level UPDATE не нужен —
|
||||
перевести на AO (Row или Column) с компрессией.
|
||||
|
||||
**Целевая раскладка storage по таблицам:**
|
||||
|
||||
| Storage | Таблицы | Почему |
|
||||
|---------|---------|--------|
|
||||
| **AO Column** zlib | `dds.dim_calendar` | Write-once (generate_series), никогда не обновляется. Колоночное хранение идеально для аналитических скан. |
|
||||
| **AO Column** zlib | `dm.route_performance` | Full rebuild (TRUNCATE+INSERT), чисто аналитические чтения. |
|
||||
| **AO Row** zlib | STG: все 9 таблиц | Уже реализовано. Append-only, иммутабельные батчи. |
|
||||
| **AO Row** zlib | ODS snapshot: `airports`, `airplanes`, `routes`, `seats` | Полный snapshot каждый раз. Перевести загрузку с UPSERT на TRUNCATE+INSERT — честнее для семантики «текущий срез». |
|
||||
| **AO Row** zlib | `dds.dim_tariffs` | Только INSERT новых тарифов, UPDATE не используется. |
|
||||
| **Heap** (явный) | ODS транзакционные: `bookings`, `tickets`, `flights`, `segments`, `boarding_passes` | Row-level UPDATE при SCD1 UPSERT. Heap обязателен. |
|
||||
| **Heap** (явный) | DDS измерения с UPDATE: `dim_airports`, `dim_airplanes`, `dim_passengers`, `dim_routes` | SCD1/SCD2 UPSERT с row-level UPDATE. |
|
||||
| **Heap** (явный) | `dds.fact_flight_sales` | UPDATE (is_boarded, seat_no меняются). |
|
||||
| **Heap** (явный) | DM витрины с UPSERT: `sales_report` и будущие HWM-витрины | Row-level UPDATE при инкрементальном UPSERT. |
|
||||
|
||||
**Учебная ценность**: студенты видят на практике три storage-стратегии в одном проекте:
|
||||
1. AO Column — для иммутабельных аналитических таблиц (dim_calendar, route_performance)
|
||||
2. AO Row — для append-only данных и snapshot-справочников (STG, ODS refs, dim_tariffs)
|
||||
3. Heap — для таблиц с row-level UPDATE (ODS транзакции, DDS dims с UPSERT, факт, DM)
|
||||
|
||||
И понимают **почему** выбор именно такой: UPDATE на AO = bloat + необходимость VACUUM.
|
||||
|
||||
---
|
||||
|
||||
## ЧТО ОСТАВИТЬ КАК ЕСТЬ
|
||||
|
||||
| Аспект | Почему не трогаем |
|
||||
@@ -155,7 +294,8 @@ GP-специфичная best practice, которую забывают даж
|
||||
|-----------|----------|--------|----------------|
|
||||
| **P0** | ODS batch resolver: разделить логику для справочников и транзакций | 2-3 часа | ODS DAG + 5 транзакционных load-скриптов |
|
||||
| ~~P0~~ | ~~Исправить distribution key в airport_traffic~~ | ~~5 мин~~ | ~~done~~ |
|
||||
| **P1** | Добавить 7 точечных комментариев | 30-40 мин | 6-7 файлов (DDL, load, DAG) |
|
||||
| ~~P1~~ | ~~Добавить 7 точечных комментариев~~ | ~~30-40 мин~~ | ~~done~~ |
|
||||
| **P2** | Явный storage type + AO где нет UPDATE (ADR-3) | 2-3 часа | все `*_ddl.sql` в ods/dds/dm + 4 ODS snapshot load |
|
||||
| **P2** | Рефакторинг hashdiff → TEMP TABLE | 1 час | `sql/dds/dim_routes_load.sql` |
|
||||
| **P2** | Переименовать STG поля в канон + заметка | 1-2 часа | 27 STG SQL + ODS load + naming_conventions.md |
|
||||
| **P2** | TEMP TABLE для сложных ODS load-ов | 1 час | 3-4 ODS load файла |
|
||||
|
||||
Reference in New Issue
Block a user