From 4a04a2caf6a9bc8d1429e441b7d27aef020c198d Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Mon, 20 Jul 2026 14:56:42 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BB=D0=B5=D1=81=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=81=D1=85=D0=B5=D0=BC,=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=B9=D0=BF=D0=BB=D0=B0=D0=B9=D0=BD=D0=B0=20=D0=B8=20=D1=82?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=BE=D0=BD=D0=BE=D0=BC=D0=B8=D1=8F=20DQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Одна диаграмма отвечает на один вопрос: db_schema.md перестроен в лесенку из четырёх схем (слои, звезда, сборка факта, витрины), полная lineage-карта перенесена в конец как справочная. Новые документы: reading_the_pipeline.md (метод чтения FROM/JOIN load-скрипта, разбор fact_src) и dq_taxonomy.md (три класса DQ-проверок на реальных скриптах, семантика NULL). Точки входа: корневой README, docs/README.md, assignment/README.md, qa-plan.md. Co-Authored-By: Claude Fable 5 --- README.md | 2 + docs/README.md | 3 + docs/assignment/README.md | 6 + docs/design/db_schema.md | 371 +++++++++++++++++----------- docs/design/reading_the_pipeline.md | 65 +++++ docs/reference/dq_taxonomy.md | 63 +++++ docs/reference/qa-plan.md | 1 + 7 files changed, 368 insertions(+), 143 deletions(-) create mode 100644 docs/design/reading_the_pipeline.md create mode 100644 docs/reference/dq_taxonomy.md diff --git a/README.md b/README.md index 5dc3d80..bed9180 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ make clean # полный reset: удалить контейнер ## Документация - [Учебные задания](docs/assignment/README.md) +- [Схема DWH: четыре уровня приближения](docs/design/db_schema.md) +- [Как читать пайплайн по скрипту загрузки](docs/design/reading_the_pipeline.md) - [План тестирования/проверок и негативные кейсы](TESTING.md) - [Дополнительные заметки и технические детали](docs/README.md) - [Детали по STG DAG](docs/bookings_to_gp_stage.md) diff --git a/docs/README.md b/docs/README.md index 85b1e57..6808ab1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ - [Быстрый старт и команды](../README.md) - [Учебные задания](assignment/README.md) +- [Как читать пайплайн по скрипту загрузки](design/reading_the_pipeline.md) - [План тестирования и проверки](../TESTING.md) - [Главный учебный DAG: bookings → stg](bookings_to_gp_stage.md) - [Учебный DAG: stg -> ods](bookings_to_gp_ods.md) @@ -16,6 +17,7 @@ - [Единые конвенции нейминга DWH (служебные поля и SCD)](design/naming_conventions.md) - [Схема БД всех слоёв DWH](design/db_schema.md) +- [Как читать пайплайн по скрипту загрузки](design/reading_the_pipeline.md) - [Дизайн-документ STG](design/bookings_stg_design.md) - [Дизайн-документ ODS](design/bookings_ods_design.md) - [Дизайн-документ DDS](design/bookings_dds_design.md) @@ -28,4 +30,5 @@ - [Как устроен Docker-стенд (образы, Connections, переменные окружения)](stack.md) - [Известные проблемы bookings-db](reference/bookings_db_issues.md) - [QA-план отладки пайплайна](reference/qa-plan.md) +- [Классы DQ-проверок](reference/dq_taxonomy.md) - [Порядок запуска DAG-ов](dag_execution_order.md) diff --git a/docs/assignment/README.md b/docs/assignment/README.md index 7bebcdd..d608434 100644 --- a/docs/assignment/README.md +++ b/docs/assignment/README.md @@ -9,6 +9,12 @@ На месте ваших заданий сейчас стоят **заглушки** (`SELECT 1;`). Вы замените их на реальную SQL-логику, и после этого весь DWH будет заполнен. +## С чего начать чтение + +Сначала посмотрите [схему DWH по уровням приближения](../design/db_schema.md). +Пока пишете свой слой, держите рядом [метод чтения скрипта загрузки](../design/reading_the_pipeline.md). +Для `*_dq.sql` сверяйтесь с [классами DQ-проверок](../reference/dq_taxonomy.md). + ## Перед началом Убедитесь, что стенд запущен и данные загружены — diff --git a/docs/design/db_schema.md b/docs/design/db_schema.md index 6ed8f58..4eccb71 100644 --- a/docs/design/db_schema.md +++ b/docs/design/db_schema.md @@ -7,157 +7,88 @@ > — заглушки (`SELECT 1;`). Данные появятся после реализации заданий. Архитектура хранилища данных (DWH) для учебного проекта Airflow + Greenplum. -Источник — демо-БД `bookings` (Postgres). Документ даёт цельный взгляд «сверху»; -детали реализации — в дизайн-документах слоёв. +Источник — демо-БД `bookings` (Postgres). Одна диаграмма отвечает на один вопрос: +карта, которая показывает всё сразу, не даёт ясного ответа ни на один из них. + +Понять, как устроен проект → лесенка ниже. + +Пишете свой load-скрипт и хотите свериться → [`reading_the_pipeline.md`](reading_the_pipeline.md). + +Какие DQ-проверки писать → [`dq_taxonomy.md`](../reference/dq_taxonomy.md). + +Точечный вопрос «что кормит X?» → полная карта в конце. --- -## Полная схема потоков данных (Data Lineage) +## Как устроен поток целиком? + +Здесь показано движение данных через пять слоёв DWH. ```mermaid 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; - classDef dm fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px; - - %% 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) - 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 - 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_Sales[dds.fact_flight_sales]:::fact - end - - %% ODS to DDS Dimensions - ODS_Airports --> DIM_Airports - ODS_Airplanes --> DIM_Airplanes - ODS_Seats -.->|total_seats| DIM_Airplanes - ODS_Segments -.->|DISTINCT| DIM_Tariffs - ODS_Tickets -->|Unique passengers| DIM_Passengers - ODS_Routes -->|SCD2 hashdiff| DIM_Routes - DIM_Airports -.->|cities| DIM_Routes - DIM_Airplanes -.->|model, seats| DIM_Routes - - %% ODS to Fact - ODS_Segments -->|Main stream| FACT_Sales - ODS_Tickets -->|book_ref, passenger_id| FACT_Sales - ODS_Bookings -->|book_date| FACT_Sales - ODS_Flights -->|schedule, route_no| FACT_Sales - ODS_Routes -->|dep/arr airports| FACT_Sales - ODS_Boarding -->|LEFT JOIN seat_no| FACT_Sales - - %% Dimensions to Fact - DIM_Calendar -->|calendar_sk| FACT_Sales - DIM_Airports -->|dep/arr _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 - - %% 5. DM (Vitrines) - subgraph DM_Layer [Layer: DM Data Marts] - direction TB - DM_Sales[dm.sales_report]:::dm - DM_Traffic[dm.airport_traffic]:::dm - DM_Route[dm.route_performance]:::dm - DM_Monthly[dm.monthly_overview]:::dm - DM_Loyalty[dm.passenger_loyalty]:::dm - end - - %% DDS to DM - FACT_Sales --> DM_Sales - FACT_Sales --> DM_Traffic - FACT_Sales --> DM_Route - FACT_Sales --> DM_Monthly - FACT_Sales --> DM_Loyalty - DIM_Airports -.-> DM_Sales - DIM_Airports -.-> DM_Traffic - DIM_Tariffs -.-> DM_Sales - DIM_Tariffs -.-> DM_Loyalty - DIM_Calendar -.-> DM_Sales - DIM_Calendar -.-> DM_Traffic - DIM_Calendar -.-> DM_Route - DIM_Calendar -.-> DM_Monthly - DIM_Calendar -.-> DM_Loyalty - DIM_Routes -.-> DM_Route - DIM_Routes -.-> DM_Monthly - DIM_Routes -.-> DM_Loyalty - DIM_Airplanes -.-> DM_Monthly - DIM_Passengers -.-> DM_Loyalty + SRC["Source
Postgres bookings"] --> STG["STG
сырые данные"] + STG --> ODS["ODS
очищенные данные и типы"] + ODS --> DDS["DDS
факты и измерения"] + DDS --> DM["DM
витрины для бизнес-вопросов"] ``` +## Как выглядит звезда? + +Здесь показан факт и шесть его измерений. + +```mermaid +graph TD + CAL["dds.dim_calendar"] --> FACT["dds.fact_flight_sales"] + AIRPORT["dds.dim_airports"] --> FACT + AIRPLANE["dds.dim_airplanes"] --> FACT + TARIFF["dds.dim_tariffs"] --> FACT + PASSENGER["dds.dim_passengers"] --> FACT + ROUTE["dds.dim_routes
SCD2"] --> FACT +``` + +## Как собирается факт? + +Здесь показаны таблицы, которые `fact_src` читает при сборке факта. +Сплошные стрелки обозначают `INNER JOIN`. Пунктирные стрелки обозначают `LEFT JOIN`. + +```mermaid +graph LR + SEG["ods.segments"] -->|"Main stream: grain, price"| FACT["dds.fact_flight_sales"] + TKT["ods.tickets"] -->|"book_ref, passenger_id"| FACT + BKG["ods.bookings"] -->|"book_date"| FACT + FLT["ods.flights"] -->|"schedule, route_no"| FACT + ODS_RTE["ods.routes"] -.->|"dep/arr airports"| FACT + CAL["dds.dim_calendar"] -.->|"calendar_sk"| FACT + AIRPORT["dds.dim_airports"] -.->|"dep/arr _sk"| FACT + AIRPLANE["dds.dim_airplanes"] -.->|"airplane_sk"| FACT + TARIFF["dds.dim_tariffs"] -.->|"tariff_sk"| FACT + PASSENGER["dds.dim_passengers"] -.->|"passenger_sk"| FACT + ROUTE["dds.dim_routes"] -.->|"route_sk"| FACT + BOARDING["ods.boarding_passes"] -.->|"LEFT JOIN seat_no"| FACT +``` + +`ods.routes` оставлен отдельным узлом: через него факт находит аэропорты +вылета и прилёта, не завися от студенческого `dim_routes`. Схема показывает +замысел загрузки. На ветке `main` поля `route_sk`, `airplane_sk` и +`passenger_sk` могут быть `NULL`, пока студент не реализовал измерения. +Почему это не всегда ошибка, объяснено в [справочнике DQ](../reference/dq_taxonomy.md#null--не-всегда-ошибка). + +## Что кормит витрины? + +Здесь показано, во что расходятся строки факта. + +```mermaid +graph LR + FACT["dds.fact_flight_sales"] -->|"день × аэропорты × тариф"| SALES["dm.sales_report
выручка и посадка"] + FACT -->|"день × аэропорт"| TRAFFIC["dm.airport_traffic
пассажиропоток"] + FACT -->|"route_bk"| ROUTE["dm.route_performance
эффективность маршрута"] + FACT -->|"месяц × airplane_sk"| MONTHLY["dm.monthly_overview
динамика по самолётам"] + FACT -->|"passenger_sk"| LOYALTY["dm.passenger_loyalty
профиль лояльности"] +``` + +Измерения уточняют даты, аэропорты, тарифы, маршруты, самолёты и пассажиров, +но основной поток во все пять витрин начинается с `fact_flight_sales`. + --- ## Сводка объектов по слоям @@ -310,5 +241,159 @@ Degenerate keys: `book_ref`, `ticket_no`, `flight_id`, `book_date`, `seat_no`. - [`bookings_dds_design.md`](bookings_dds_design.md) — дизайн DDS (Star Schema, SCD2) - [`bookings_dm_design.md`](bookings_dm_design.md) — дизайн DM (5 витрин) - [`naming_conventions.md`](naming_conventions.md) — нейминг полей +- [`reading_the_pipeline.md`](reading_the_pipeline.md) — как читать связи из скрипта загрузки +- [`../reference/dq_taxonomy.md`](../reference/dq_taxonomy.md) — классы DQ-проверок и допустимые NULL - Часовые пояса и настройка PXF — в ветке `solution` (`docs/reference/`) - [`../assignment/analyst_spec.md`](../assignment/analyst_spec.md) — ТЗ от аналитика (курсовое задание) + +--- + +## Полная карта lineage (справочная) + +Здесь собран цельный взгляд «сверху». Эта карта нужна для точечного поиска: +например, «что кормит X?» или «куда идут данные из X?». Читать весь пайплайн +по ней подряд не нужно. + +```mermaid +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; + classDef dm fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px; + + %% 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) + 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 + 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_Sales[dds.fact_flight_sales]:::fact + end + + %% ODS to DDS Dimensions + ODS_Airports --> DIM_Airports + ODS_Airplanes --> DIM_Airplanes + ODS_Seats -.->|total_seats| DIM_Airplanes + ODS_Segments -.->|DISTINCT| DIM_Tariffs + ODS_Tickets -->|Unique passengers| DIM_Passengers + ODS_Routes -->|SCD2 hashdiff| DIM_Routes + DIM_Airports -.->|cities| DIM_Routes + DIM_Airplanes -.->|model, seats| DIM_Routes + + %% ODS to Fact + ODS_Segments -->|Main stream| FACT_Sales + ODS_Tickets -->|book_ref, passenger_id| FACT_Sales + ODS_Bookings -->|book_date| FACT_Sales + ODS_Flights -->|schedule, route_no| FACT_Sales + ODS_Routes -->|dep/arr airports| FACT_Sales + ODS_Boarding -->|LEFT JOIN seat_no| FACT_Sales + + %% Dimensions to Fact + DIM_Calendar -->|calendar_sk| FACT_Sales + DIM_Airports -->|dep/arr _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 + + %% 5. DM (Vitrines) + subgraph DM_Layer [Layer: DM Data Marts] + direction TB + DM_Sales[dm.sales_report]:::dm + DM_Traffic[dm.airport_traffic]:::dm + DM_Route[dm.route_performance]:::dm + DM_Monthly[dm.monthly_overview]:::dm + DM_Loyalty[dm.passenger_loyalty]:::dm + end + + %% DDS to DM + FACT_Sales --> DM_Sales + FACT_Sales --> DM_Traffic + FACT_Sales --> DM_Route + FACT_Sales --> DM_Monthly + FACT_Sales --> DM_Loyalty + DIM_Airports -.-> DM_Sales + DIM_Airports -.-> DM_Traffic + DIM_Tariffs -.-> DM_Sales + DIM_Tariffs -.-> DM_Loyalty + DIM_Calendar -.-> DM_Sales + DIM_Calendar -.-> DM_Traffic + DIM_Calendar -.-> DM_Route + DIM_Calendar -.-> DM_Monthly + DIM_Calendar -.-> DM_Loyalty + DIM_Routes -.-> DM_Route + DIM_Routes -.-> DM_Monthly + DIM_Routes -.-> DM_Loyalty + DIM_Airplanes -.-> DM_Monthly + DIM_Passengers -.-> DM_Loyalty +``` diff --git a/docs/design/reading_the_pipeline.md b/docs/design/reading_the_pipeline.md new file mode 100644 index 0000000..93bb085 --- /dev/null +++ b/docs/design/reading_the_pipeline.md @@ -0,0 +1,65 @@ +# Как читать пайплайн по скрипту загрузки + +Большую схему не нужно запоминать. Связи любой таблицы уже записаны в блоке +`FROM ... JOIN ...` её файла `*_load.sql`. Обычно каждая прочитанная таблица +даёт одно входящее ребро. Если одна таблица читается под разными псевдонимами, +как `dim_airports`, её можно показать одним узлом. + +## Метод + +`INNER JOIN` показывает обязательную часть основного потока. Если входной строки +нет, целевая строка не появится. `LEFT JOIN` к измерениям решает две задачи. +Факт может прийти раньше измерения: это опаздывающее измерение +(late-arriving dimension). Эталонный факт также не должен зависеть от того, +успел ли студент реализовать свои измерения. Поэтому строка факта сохраняется, +а её суррогатный ключ пока может быть `NULL`. + +В этом CTE есть и другие `LEFT JOIN`. `ods.routes` даёт эталонный путь к +аэропортам, а `ods.boarding_passes` содержит необязательные `seat_no` и +признак посадки. + +Если в загрузчике несколько SQL-операторов, смотрите на источник `INSERT`. +Именно он задаёт полный список входящих связей для новых строк. Например, в +`fact_flight_sales_load.sql` блок `FROM` у `UPDATE` читает только изменяемые +поля, а полный список находится в CTE `fact_src` перед `INSERT`. + +## Пример: `fact_flight_sales` + +Откройте [`sql/dds/fact_flight_sales_load.sql`](../../sql/dds/fact_flight_sales_load.sql) +и пройдите `fact_src` сверху вниз: + +1. `FROM ods.segments` задаёт зерно `(ticket_no, flight_id)` и поле `price`. +2. `JOIN ods.tickets` обязателен для `book_ref` и `passenger_id`. +3. `JOIN ods.bookings` обязателен для `book_date`. +4. `JOIN ods.flights` обязателен для расписания и `route_no`. +5. `LEFT JOIN ods.routes` находит аэропорты по `route_no`. Этот эталонный путь + не зависит от студенческого `dim_routes`. +6. `LEFT JOIN dds.dim_routes` ищет point-in-time версию маршрута и `route_sk`. +7. `LEFT JOIN dds.dim_calendar` даёт `calendar_sk` по дате вылета. +8. Два `LEFT JOIN dds.dim_airports` дают ключи аэропортов вылета и прилёта. + На схеме их можно показать одним узлом `dim_airports`. +9. `LEFT JOIN dds.dim_airplanes` даёт `airplane_sk` через найденную версию маршрута. +10. `LEFT JOIN dds.dim_tariffs` даёт `tariff_sk`. +11. `LEFT JOIN dds.dim_passengers` даёт `passenger_sk`. +12. `LEFT JOIN ods.boarding_passes` даёт `seat_no` и признак `is_boarded`. + +Так список связей превращается в [схему «Как собирается факт?»](db_schema.md#как-собирается-факт): +четыре обязательных входа рисуются сплошными стрелками, а остальные пунктирными. + +## Как проверить свой слой + +- Выпишите все таблицы из `FROM` и `JOIN` своего `*_load.sql`. +- Сравните список с эталонной схемой или дизайн-документом этого слоя. +- Проверьте служебные и SCD-поля по [`naming_conventions.md`](naming_conventions.md). +- Убедитесь, что рядом есть `*_dq.sql` и он покрывает нужные + [классы DQ-проверок](../reference/dq_taxonomy.md). + +## Шаблон схемы одной таблицы + +Скопируйте шаблон и замените имена таблиц и полей: + +```mermaid +graph LR + MAIN["schema.main_input"] -->|"grain, metric"| TARGET["schema.target"] + LOOKUP["schema.lookup"] -.->|"lookup_key"| TARGET +``` diff --git a/docs/reference/dq_taxonomy.md b/docs/reference/dq_taxonomy.md new file mode 100644 index 0000000..c2a2e60 --- /dev/null +++ b/docs/reference/dq_taxonomy.md @@ -0,0 +1,63 @@ +# Классы DQ-проверок + +DQ-проверка должна отвечать на конкретный вопрос. В этом проекте есть три +класса. Один `*_dq.sql` может содержать проверки нескольких классов. + +## 1. Универсальные структурные проверки + +Они проверяют форму таблицы: обязательные ключи заполнены, ключ уникален, а +таблица не пуста там, где по контракту ожидаются строки. Так можно быстро поймать +сломанное зерно или незавершённую загрузку без знания бизнес-смысла данных. + +Реальные примеры: + +- [`sql/dds/dim_airports_dq.sql`](../../sql/dds/dim_airports_dq.sql) требует + непустую таблицу, проверяет уникальность `airport_sk` и `airport_bk`, а также + обязательные поля. +- [`sql/ods/bookings_dq.sql`](../../sql/ods/bookings_dq.sql) ищет дубли + `book_ref` и `NULL` или пустые значения в обязательных полях. Пустой + инкрементальный батч здесь допустим. +- [`sql/ods/tickets_dq.sql`](../../sql/ods/tickets_dq.sql) проверяет уникальность + `ticket_no`, обязательные поля и отсутствие билетов без `bookings.book_ref`. + +## 2. Проверки бизнес-правил + +Они проверяют ограничения предметной области, которых не видно из структуры +таблицы. Такая проверка нужна, чтобы заметить правдоподобную по типам, но +невозможную по смыслу строку. + +В [`sql/dm/sales_report_dq.sql`](../../sql/dm/sales_report_dq.sql) для дат +текущего запуска проверяется, что `tickets_sold >= passengers_boarded`, +`boarding_rate` лежит между `0` и `1`, а `total_revenue` неотрицательна. + +## 3. Межслойные сверки + +Они отвечают на вопрос: согласован ли слой N+1 со слоем N для нужного батча? +Сверка находит потерянные или лишние строки на границе загрузки. + +Реальные примеры используют разные способы сверки: + +- [`sql/ods/airports_dq.sql`](../../sql/ods/airports_dq.sql) сравнивает множества + `airport_code`: ищет пропущенные и лишние ключи относительно текущего STG-батча. + Это сверка ключей, а не простое сравнение числа строк. +- [`sql/stg/airplanes_dq.sql`](../../sql/stg/airplanes_dq.sql) сравнивает число + строк во внешней `stg.airplanes_ext` и в текущем батче `stg.airplanes`. +- [`sql/dds/fact_flight_sales_dq.sql`](../../sql/dds/fact_flight_sales_dq.sql) + требует равенства числа строк факта и `ods.segments`, потому что зерно факта — + один сегмент билета. + +## `NULL` — не всегда ошибка + +В [`fact_flight_sales_load.sql`](../../sql/dds/fact_flight_sales_load.sql) +измерения присоединяются через `LEFT JOIN`. Факт может прийти раньше измерения, +поэтому `route_sk`, `airplane_sk` или `passenger_sk` временно бывают `NULL`. На +ветке `main` это также ожидаемо, пока студент не реализовал эти измерения. Такой +`NULL` сохраняет строку факта и не должен сам по себе останавливать пайплайн. + +`seat_no` тоже может быть `NULL`: у билета уже есть сегмент, но записи +`boarding_passes` ещё нет. Обязательный ключ, напротив, должен быть заполнен. +Например, `NULL` в `airport_sk` измерения или в `(ticket_no, flight_id)` факта +нарушает контракт таблицы. + +Архитектурное решение о том, где живут DQ-проверки, зафиксировано в разделе +[«Ключевые договорённости»](../design/db_schema.md#ключевые-договорённости). diff --git a/docs/reference/qa-plan.md b/docs/reference/qa-plan.md index 78f89c3..84a3ee2 100644 --- a/docs/reference/qa-plan.md +++ b/docs/reference/qa-plan.md @@ -14,6 +14,7 @@ > > Зависимость: перед запуском этого плана нужно починить bookings-db > (см. `docs/reference/bookings_db_issues.md`). +> Классы проверок для разбора ошибок: [`dq_taxonomy.md`](dq_taxonomy.md). ---