- Зачем:
- STG-слой использовал legacy-имена (batch_id, load_dttm, src_created_at_ts),
тогда как ODS/DDS/DM уже работали с каноном (_load_id, _load_ts, event_ts).
Студент видел разные имена для одного понятия — это убрано.
- Что:
- переименованы колонки в 9 STG DDL: batch_id→_load_id, load_dttm→_load_ts,
src_created_at_ts→event_ts; добавлен NOT NULL для _load_id во всех таблицах.
- обновлены 9 STG Load, 9 STG DQ, 9 ODS Load, 9 ODS DQ (INSERT/SELECT/WHERE).
- обновлены DAG-файлы bookings_to_gp_stage.py и bookings_to_gp_ods.py
(встроенный SQL резолвера, комментарии; Python-идентификаторы не тронуты).
- обновлены тесты и ~15 документов (naming_conventions, PRD, db_schema,
design-docs, qa-plan, README, TESTING и др.).
- Проверка:
- grep -rn 'load_dttm\|src_created_at_ts' sql/ airflow/ tests/ — 0 совпадений.
- make test — 4 passed.
- e2e-etl: day1 прошёл полностью, day2 стартовал без ошибок.
30 KiB
Схема БД DWH (Bookings → Greenplum)
Статус: Проект в разработке. Реализованы STG, ODS и DDS (bookings).
Обзор
Эта документация описывает архитектуру хранилища данных (DWH) для учебного проекта Airflow + Greenplum. Источник данных — демо-БД bookings (Postgres).
Целевые аудитории:
- LLM/Разработчики: Технические спецификации для реализации (см. раздел "Спецификации для реализации")
- Студенты: Обучающие материалы и пояснения (см. раздел "Обучающие материалы")
Спецификации для реализации (для LLM и разработчиков)
Ключевые договорённости
- Источник: используем основные таблицы схемы
bookings(табличные данные, неVIEW) - Зерно факта
dds.fact_flight_sales: 1 строка = 1 сегмент билета (ticket_no+flight_id, источник:segments) - Обязательная связь для аэропортов и самолёта:
flights.route_no → routes → (departure_airport, arrival_airport, airplane_code) - Маршруты в DDS: используем
dds.dim_routes(SCD2), в факт пишемroute_skчерез point-in-time lookup на дату вылета - Даты: как минимум различаем
book_date(дата покупки) иscheduled_departure(дата/время вылета) - Инкремент в STG: для
ticketsопорная дата берётся изbookings.book_date, потому что вticketsнет собственного поля времени изменения - DQ-проверки: проверки качества данных выполняем SQL-скриптами, но не сохраняем результаты в отдельные таблицы/слой DQ (при проблемах падаем с понятной ошибкой и останавливаем пайплайн)
- Нейминг полей: единый стандарт — в
docs/internal/naming_conventions.md
Статус реализации по слоям
| Слой | Статус | Реализовано |
|---|---|---|
| Source | ✅ Готово | Демо-БД bookings (Postgres) |
| STG | ✅ Готово | 9 из 9 таблиц (bookings, tickets, airports, airplanes, routes, seats, flights, segments, boarding_passes) |
| ODS | ✅ Готово | 9 из 9 таблиц + DAG bookings_ods_ddl и bookings_to_gp_ods |
| DDS | ✅ Готово | 6 измерений + 1 факт + DAG bookings_dds_ddl и bookings_to_gp_dds |
Архитектура слоёв
STG (Staging Layer)
- Назначение: Сырой слой, максимально близкий к источнику, без бизнес-логики
- Хранение: AO-Row (Append-Only Row-oriented) для эффективной загрузки больших объёмов
- Типы данных: Бизнес-колонки как
TEXT, тех.колонки какTIMESTAMP - Инкрементальная загрузка: Опорное поле
event_ts(изbook_dateдля tickets) - Технологические колонки:
event_ts TIMESTAMP— дата/время из источника для инкремента_load_ts TIMESTAMP NOT NULL DEFAULT now()— когда запись была загружена_load_id TEXT— идентификатор пачки/батча (например{{ run_id }})
- DQ-проверки (после загрузки STG): отдельные SQL-скрипты, которые валидируют данные (counts, дубли, NULL, orphan records) и при ошибке делают
RAISE EXCEPTION; примеры:sql/stg/bookings_dq.sql,sql/stg/tickets_dq.sql
ODS (Operational Data Store)
- Назначение: Очищенные данные в 3NF, готовые для аналитики
- Хранение: Heap для частых чтений и обновлений
- Трансформации: Очистка, приведение типов, нормализация, SCD1 UPSERT
- Связи: Все связи через бизнес-ключи (без суррогатных ключей)
- Текущий статус: Реализован (9 таблиц, SQL DQ, DAG загрузки)
DDS (Data Delivery System)
- Назначение: Star Schema для аналитики и отчётности
- Хранение: Heap или AO-CO (Append-Only Column-oriented) для аналитических запросов
- Структура: Измерения (Dimensions) + Факты (Facts)
- Ключи: Суррогатные ключи (SK) для измерений, FK в фактах
- Текущий статус: Реализован (6 измерений, 1 факт, SQL DQ, DAG загрузки)
Измерения DDS (Dimensions)
| Измерение | Бизнес-ключ | Суррогатный ключ | Атрибуты |
|---|---|---|---|
dds.dim_calendar |
date_actual |
calendar_sk |
year_actual, month_actual, day_actual, day_of_week, day_name, is_weekend |
dds.dim_airports |
airport_code (airport_bk) |
airport_sk |
airport_name, city, country, timezone, coordinates |
dds.dim_airplanes |
airplane_code (airplane_bk) |
airplane_sk |
model, range_km, speed_kmh, total_seats |
dds.dim_tariffs |
fare_conditions |
tariff_sk |
fare_conditions |
dds.dim_passengers |
passenger_id (passenger_bk) |
passenger_sk |
passenger_name (SCD1) |
dds.dim_routes |
route_no (route_bk) |
route_sk |
departure_airport, arrival_airport, airplane_code, departure_city (денормализовано), arrival_city (денормализовано), airplane_model (денормализовано), total_seats (денормализовано), hashdiff, valid_from, valid_to (SCD2) |
Факт DDS (Fact)
dds.fact_flight_sales:
- Зерно: 1 строка = 1 сегмент билета (
ticket_no+flight_id) - FK на измерения:
calendar_sk— ссылка на дату вылетаdeparture_airport_sk— аэропорт вылетаarrival_airport_sk— аэропорт прилётаairplane_sk— самолётtariff_sk— тарифpassenger_sk— пассажирroute_sk— версия маршрута (SCD2, point-in-time)
- Метрики:
price NUMERIC— стоимость сегментаis_boarded BOOLEAN— сел ли пассажир в самолёт (из boarding_passes)
- Атрибуты:
book_ref TEXT— бизнес-ключ бронированияbook_date DATE— дата покупкиticket_no TEXT— номер билетаflight_id INT— ID рейсаseat_no TEXT— место (если есть)
Детальное описание таблиц STG слоя
stg.bookings (транзакции, инкремент)
- Источник:
bookings.bookings(через PXF) - Ключ распределения:
book_ref - Бизнес-колонки:
book_ref TEXT- номер бронированияbook_date TEXT- дата бронированияtotal_amount TEXT- общая сумма
- Технические колонки:
event_ts(=book_date),_load_ts,_load_id - Стратегия загрузки: Инкремент по
book_date - DQ проверки: count (окно инкремента, пустое окно допустимо), дубликаты book_ref, NULL обязательных полей
stg.tickets (транзакции, инкремент)
- Источник:
bookings.tickets(через PXF) - Ключ распределения:
book_ref - Примечание: JOIN
tickets↔segments/boarding_passesпоticket_noможет требовать motion (ключи распределения разные). - Бизнес-колонки:
ticket_no TEXT- номер билетаbook_ref TEXT- номер бронированияpassenger_id TEXT- идентификатор пассажираpassenger_name TEXT- имя пассажираoutbound TEXT- направление (в источнике boolean)
- Технические колонки:
event_ts(из book_date через bookings),_load_ts,_load_id - Стратегия загрузки: Инкремент по
book_date(через bookings) - DQ проверки: count (окно инкремента, пустое окно допустимо), дубликаты ticket_no, NULL обязательных полей, пустой passenger_name, ссылочная целостность (bookings)
stg.airports (справочник, full load)
- Источник:
bookings.airports_data(через PXF) - Ключ распределения:
airport_code - Бизнес-колонки:
airport_code TEXT- код аэропортаairport_name TEXT- название аэропорта (из JSONB)city TEXT- город (из JSONB)country TEXT- страна (из JSONB)coordinates TEXT- координатыtimezone TEXT- часовой пояс
- Технические колонки:
event_ts(=now()),_load_ts,_load_id - Стратегия загрузки: Full load (все строки при каждом запуске)
- DQ проверки: count, дубликаты airport_code, NULL обязательных полей
stg.airplanes (справочник, full load)
- Источник:
bookings.airplanes_data(через PXF) - Ключ распределения:
airplane_code - Бизнес-колонки:
airplane_code TEXT- код самолётаmodel TEXT- модель (из JSONB)range TEXT- дальность полётаspeed TEXT- скорость
- Технические колонки:
event_ts(=now()),_load_ts,_load_id - Стратегия загрузки: Full load
- DQ проверки: count, дубликаты airplane_code, NULL обязательных полей
stg.routes (справочник, full load)
- Источник:
bookings.routes(через PXF) - Ключ распределения:
route_no - Примечание: JOIN по
departure_airport/arrival_airport/airplane_codeможет требовать motion (ключи распределения разные). - Бизнес-колонки:
route_no TEXT- номер маршрутаvalidity TEXT- период действия (из tstzrange)departure_airport TEXT- аэропорт вылетаarrival_airport TEXT- аэропорт прилётаairplane_code TEXT- код самолётаdays_of_week TEXT- дни недели (из int[])scheduled_time TEXT- плановое времяduration TEXT- длительность
- Технические колонки:
event_ts(=now()),_load_ts,_load_id - Стратегия загрузки: Full load
- DQ проверки: count, дубликаты (route_no, validity), NULL обязательных полей, ссылочная целостность (_load_id = текущий батч)
stg.seats (справочник, full load)
- Источник:
bookings.seats(через PXF) - Ключ распределения:
airplane_code(co-location с airplanes) - Бизнес-колонки:
airplane_code TEXT- код самолётаseat_no TEXT- номер местаfare_conditions TEXT- класс обслуживания
- Технические колонки:
event_ts(=now()),_load_ts,_load_id - Стратегия загрузки: Full load
- DQ проверки: count, дубликаты (airplane_code, seat_no), NULL обязательных полей, ссылочная целостность (_load_id = текущий батч)
stg.flights (транзакции, инкремент)
- Источник:
bookings.flights(через PXF) - Ключ распределения:
flight_id - Примечание: JOIN с таблицами, распределёнными по другим ключам, может требовать motion.
- Бизнес-колонки:
flight_id TEXT- идентификатор рейсаroute_no TEXT- номер маршрутаstatus TEXT- статусscheduled_departure TEXT- плановое время вылетаscheduled_arrival TEXT- плановое время прилётаactual_departure TEXT- фактическое время вылетаactual_arrival TEXT- фактическое время прилёта
- Технические колонки:
event_ts(=scheduled_departure),_load_ts,_load_id - Стратегия загрузки: Инкремент по
scheduled_departure - DQ проверки: count (окно инкремента, пустое окно допустимо), дубликаты flight_id, NULL обязательных полей, ссылочная целостность (routes, _load_id = текущий батч)
stg.segments (транзакции, инкремент)
- Источник:
bookings.segments(через PXF) - Ключ распределения:
ticket_no(co-location с boarding_passes) - Примечание: JOIN
segments↔ticketsпоticket_noможет требовать motion (stg.tickets распределена поbook_ref). - Бизнес-колонки:
ticket_no TEXT- номер билетаflight_id TEXT- идентификатор рейсаfare_conditions TEXT- класс обслуживанияprice TEXT- цена
- Технические колонки:
event_ts(из book_date через tickets),_load_ts,_load_id - Стратегия загрузки: Инкремент по
book_date(через tickets) - DQ проверки: count (окно инкремента, пустое окно допустимо), дубликаты (ticket_no, flight_id), NULL обязательных полей, ссылочная целостность (tickets, flights)
stg.boarding_passes (транзакции, full snapshot)
- Источник:
bookings.boarding_passes(через PXF) - Ключ распределения:
ticket_no(co-location с segments) - Примечание: JOIN
boarding_passes↔ticketsпоticket_noможет требовать motion (stg.tickets распределена поbook_ref). - Бизнес-колонки:
ticket_no TEXT- номер билетаflight_id TEXT- идентификатор рейсаseat_no TEXT- номер местаboarding_no TEXT- номер посадкиboarding_time TEXT- время посадки
- Технические колонки:
event_ts(=now()),_load_ts,_load_id - Стратегия загрузки: Full snapshot (все строки при каждом запуске)
- DQ проверки: count, дубликаты (ticket_no, flight_id), NULL обязательных полей, ссылочная целостность
Полная схема потоков данных (Data Lineage)
graph LR
%% Стили
classDef source fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
classDef stg fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
classDef ods fill:#e0f2f1,stroke:#00695c,stroke-width:2px;
classDef dim fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
classDef fact fill:#ffccbc,stroke:#bf360c,stroke-width:4px;
%% 1. Source
subgraph Source_Postgres [Source: Postgres Bookings]
direction TB
SRC_Airports[airports_data]:::source
SRC_Airplanes[airplanes_data]:::source
SRC_Routes[routes]:::source
SRC_Seats[seats]:::source
SRC_Bookings[bookings]:::source
SRC_Tickets[tickets]:::source
SRC_Flights[flights]:::source
SRC_Segments[segments]:::source
SRC_Boarding[boarding_passes]:::source
end
%% 2. STAGING (Load 1-to-1, AO-Row)
subgraph STG_Layer [Layer: STG Staging]
direction TB
STG_Airports[stg.airports]:::stg
STG_Airplanes[stg.airplanes]:::stg
STG_Routes[stg.routes]:::stg
STG_Seats[stg.seats]:::stg
STG_Bookings[stg.bookings]:::stg
STG_Tickets[stg.tickets]:::stg
STG_Flights[stg.flights]:::stg
STG_Segments[stg.segments]:::stg
STG_Boarding[stg.boarding_passes]:::stg
end
%% Links Source to STG
SRC_Airports --> STG_Airports
SRC_Airplanes --> STG_Airplanes
SRC_Routes --> STG_Routes
SRC_Seats --> STG_Seats
SRC_Bookings --> STG_Bookings
SRC_Tickets --> STG_Tickets
SRC_Flights --> STG_Flights
SRC_Segments --> STG_Segments
SRC_Boarding --> STG_Boarding
%% 3. ODS (3NF, Clean, Type, Heap)
subgraph ODS_Layer [Layer: ODS Operational Core]
direction TB
ODS_Airports[ods.airports]:::ods
ODS_Airplanes[ods.airplanes]:::ods
ODS_Routes[ods.routes]:::ods
ODS_Seats[ods.seats]:::ods
ODS_Bookings[ods.bookings]:::ods
ODS_Tickets[ods.tickets]:::ods
ODS_Flights[ods.flights]:::ods
ODS_Segments[ods.segments]:::ods
ODS_Boarding[ods.boarding_passes]:::ods
end
%% Links STG to ODS
STG_Airports --> ODS_Airports
STG_Airplanes --> ODS_Airplanes
STG_Routes --> ODS_Routes
STG_Seats --> ODS_Seats
STG_Bookings --> ODS_Bookings
STG_Tickets --> ODS_Tickets
STG_Flights --> ODS_Flights
STG_Segments --> ODS_Segments
STG_Boarding --> ODS_Boarding
%% 4. DDS (Star Schema)
subgraph DDS_Layer [Layer: DDS Star Schema]
direction TB
%% Dimensions
DIM_Calendar[dds.dim_calendar]:::dim
DIM_Airports[dds.dim_airports]:::dim
DIM_Airplanes[dds.dim_airplanes]:::dim
DIM_Tariffs[dds.dim_tariffs]:::dim
DIM_Passengers[dds.dim_passengers]:::dim
DIM_Routes[dds.dim_routes SCD2]:::dim
%% Fact
FACT_Sales[dds.fact_flight_sales]:::fact
end
%% Transformations ODS to DDS
%% Form reference tables
ODS_Airports --> DIM_Airports
ODS_Airplanes --> DIM_Airplanes
ODS_Seats -.->|Enrich total_seats| DIM_Airplanes
ODS_Segments -.->|Extract distinct| DIM_Tariffs
ODS_Tickets -->|Extract Unique| DIM_Passengers
ODS_Routes -->|SCD2 with hashdiff| DIM_Routes
DIM_Airports -.->|Enrich cities| DIM_Routes
DIM_Airplanes -.->|Enrich model and seats| DIM_Routes
%% Fact assembly (Main process + route point-in-time)
ODS_Segments -->|Main Stream| FACT_Sales
ODS_Tickets -->|Join book_ref passenger_id| FACT_Sales
ODS_Bookings -->|Join book_date| FACT_Sales
ODS_Flights -->|Join Times Status Route No| FACT_Sales
ODS_Boarding -->|LEFT JOIN Seat No| FACT_Sales
%% Link dimensions to fact
DIM_Calendar -->|calendar_sk| FACT_Sales
DIM_Airports -->|departure_airport_sk| FACT_Sales
DIM_Airports -->|arrival_airport_sk| FACT_Sales
DIM_Airplanes -->|airplane_sk| FACT_Sales
DIM_Tariffs -->|tariff_sk| FACT_Sales
DIM_Passengers -->|passenger_sk| FACT_Sales
DIM_Routes -->|route_sk| FACT_Sales
Обучающие материалы (для студентов)
Глоссарий ключевых терминов
| Термин | Объяснение |
|---|---|
| Зерно факта (Fact Grain) | Минимальная единица измерения в факте. Для dds.fact_flight_sales — это один сегмент билета. |
| Суррогатный ключ (Surrogate Key, SK) | Технический ключ (обычно INT), который генерируется в DWH и не зависит от бизнес-ключа. |
| Бизнес-ключ (Business Key) | Ключ из источника (например, airport_code, passenger_id). |
| Star Schema | Модель данных, где факт в центре, а измерения вокруг него (как звезда). |
| SCD Type 1 | Slowly Changing Dimension Type 1: при изменении данных просто перезаписываем старую запись. |
| SCD Type 2 | Slowly Changing Dimension Type 2: при изменении данных создаём новую запись с датой начала/действия. |
| AO-Row | Append-Only Row-oriented: хранение данных по строкам, только добавление (без UPDATE/DELETE). |
| Heap | Обычное хранение данных (как в обычной таблице), поддерживает UPDATE/DELETE. |
| Инкрементальная загрузка | Загрузка только новых/изменённых данных за период, а не всей таблицы. |
Пояснения к схеме
Эта диаграмма покрывает основные таблицы источника и показывает логику их трансформации. Вот на что стоит обратить внимание при обучении:
1. Ветка справочников (Reference Data)
seats + airplanes → dds.dim_airplanes: Здесь мы показываем пример обогащения. Таблица seats сама по себе в аналитике редко нужна отдельной сущностью. Мы используем её в ODS, чтобы посчитать общее количество мест (total_seats) и добавить это как атрибут в измерение самолётов (dds.dim_airplanes).
airports → dds.dim_airports: Простой перенос (1-в-1), но в DDS мы можем добавить, например, поле city_ru и city_en как отдельные колонки, убрав JSON, который есть в источнике.
2. Ветка генерации измерений (Dimension Generation)
tickets → dds.dim_passengers: Это самая сложная трансформация для измерения. В источнике нет таблицы "Пассажиры". Мы должны объяснить студентам, что мы "майним" пассажиров из билетов. Важно: один и тот же пассажир может иметь разные записи с разными именами (опечатки, изменение фамилии), поэтому в проде часто делают логику SCD Type 2 для отслеживания изменений.
- Для домашки (и первого эталонного решения) обычно достаточно SCD Type 1: одна актуальная запись на
passenger_id, а SCD2 можно оставить как усложнение.
segments → dds.dim_tariffs: Таблицы тарифов физически нет в источнике, она хранится строкой (fare_conditions: Economy/Comfort/Business) в таблице segments. Мы выносим её в отдельный справочник (нормализация), чтобы в факте хранить маленький INT ключ, а не длинную строку.
routes → dds.dim_routes (SCD2): Это отдельный учебный пример историзации. По route_no храним версии маршрута с valid_from/valid_to и hashdiff, чтобы показать студентам паттерн SCD2 на практике.
3. Сборка Факта (dds.fact_flight_sales)
Это центр звезды. Мы собираем его из шести ODS таблиц:
ods.segments: Основа (зерно факта — один полётный сегмент билета). Дает стоимость (price).ods.tickets: Приджойниваем, чтобы получитьbook_refиpassenger_id.ods.bookings: Приджойниваем поbook_ref, чтобы получитьbook_date(дата покупки).ods.flights: Приджойниваем, чтобы получить расписание/факт времени и статус рейса, а такжеroute_no(связка на маршруты).dds.dim_routes: Поroute_noи дате вылета подбираем версию маршрута (point-in-time) и получаемroute_sk.ods.boarding_passes: Приджойниваем (LEFT JOIN), чтобы узнать, сел ли пассажир реально в самолёт и на какое место (seat_no). Это важный бизнес-аспект: билет куплен, но посадочный не выдан = пассажир не летел.
4. Почему нет dds.dim_bookings?
В классической Star Schema измерения — это справочники (airports, airplanes, passengers), а факты — транзакции/события (sales, bookings).
bookings — это транзакционная таблица, а не справочник. Вместо отдельного измерения dds.dim_bookings мы храним:
book_ref— бизнес-ключ бронирования (в факте)book_date— дата бронирования (в факте, берём изods.bookingsпоbook_ref)
Это позволяет отвечать на вопросы типа: "За сколько дней до вылета люди обычно покупают билеты?" (разница между book_date и датой вылета из dds.dim_calendar).
5. Суррогатные ключи (Surrogate Keys)
В Star Schema факт должен ссылаться на суррогатные ключи (SK) измерений, а не на бизнес-ключи:
| Бизнес-ключ | Суррогатный ключ | Преимущество |
|---|---|---|
airport_code CHAR(3) |
airport_sk INT |
Меньший размер, стабильность |
airplane_code TEXT |
airplane_sk INT |
Меньший размер, стабильность |
passenger_id TEXT |
passenger_sk INT |
Меньший размер, отслеживание изменений |
Связанные документы
docs/internal/bookings_stg_design.md— Детальный дизайн STG слоя для bookingsdocs/internal/bookings_ods_design.md— Детальный дизайн ODS слоя (SCD1, batch contract, DQ)docs/internal/bookings_dds_design.md— План реализации DDS слоя (Star Schema, SCD2 для routes)docs/bookings_to_gp_dds.md— Запуск и проверка DAGbookings_to_gp_ddsdocs/internal/bookings_stg_code_review.md— Ревью решения и рекомендации по улучшениюdocs/internal/bookings_tz.md— Работа с часовыми поясами в источникеdocs/internal/pxf_bookings.md— Настройка PXF для чтения из bookings-dbTESTING.md— Пошаговый чек-лист для тестирования стенда
История изменений
| Дата | Версия | Описание изменений |
|---|---|---|
| 2026-02-25 | 2.2 | Реализован DDS: добавлены sql/dds/* (DDL/LOAD/DQ), DAG bookings_dds_ddl, DAG bookings_to_gp_dds, обновлены smoke-тесты и документация. |
| 2026-02-23 | 2.1 | Актуализирован статус: STG+ODS реализованы. Обновлены DDS-объекты (dds.dim_*, dds.fact_flight_sales), добавлен dds.dim_routes (SCD2), исправлены диаграмма и TODO. |
| 2025-01-17 | 2.0 | Удалён слой DQ для упрощения учебного стенда. Добавлены спецификации для LLM и обучающие материалы для студентов. Добавлен глоссарий терминов. |
| 2025-01-17 | 1.1 | Исправлены названия таблиц (aircrafts_data → airplanes_data, ticket_flights → segments), удалено dim.bookings, добавлены суррогатные ключи, добавлен слой DQ, исправлены связи |
| 2025-01-XX | 1.0 | Первоначальная версия |
TODO
- Реализовать STG слой полностью (все 9 таблиц)
- Реализовать ODS слой
- Реализовать DDS слой (измерения и факт)
- Создать DAG для загрузки ODS
- Создать DAG для загрузки DDS