From a8148c8afa0063df099d5826d0c8e3c68a421b40 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Thu, 12 Mar 2026 22:51:55 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=86=D0=B8=D1=8F=20=E2=80=94=20airport=20lookup?= =?UTF-8?q?=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20ods.routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Описания fact_flight_sales_load обновлены в bookings_dds_design, bookings_to_gp_dds и db_schema: departure/arrival_airport_sk теперь разрешаются через ods.routes → dim_airports, а не через dim_routes. Co-Authored-By: Claude Opus 4.6 --- docs/bookings_to_gp_dds.md | 8 +++++-- docs/design/bookings_dds_design.md | 36 +++++++++++++++++++++--------- docs/design/db_schema.md | 11 ++++----- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/docs/bookings_to_gp_dds.md b/docs/bookings_to_gp_dds.md index d2f28b7..6ff4808 100644 --- a/docs/bookings_to_gp_dds.md +++ b/docs/bookings_to_gp_dds.md @@ -123,8 +123,12 @@ DQ-проверки факта отражают эту логику: > В боевых системах для этого используют «строку-заглушку» (unknown member, SK = 0) > и отдельный процесс backfill. -**Point-in-time join для SCD2.** -Для `dim_routes` используется привязка по дате рейса: +**Два пути lookup для аэропортов и маршрутов.** +Аэропорты (`departure_airport_sk`, `arrival_airport_sk`) разрешаются через `ods.routes` → +`dim_airports`. Аэропорты вылета/прилёта одинаковы во всех версиях маршрута, поэтому +point-in-time логика не нужна — безопасно брать актуальную версию из ODS. + +`route_sk` и `airplane_sk` разрешаются через point-in-time join с SCD2 `dim_routes`: ```sql LEFT JOIN dds.dim_routes AS rte diff --git a/docs/design/bookings_dds_design.md b/docs/design/bookings_dds_design.md index 985e5b9..7fa97d5 100644 --- a/docs/design/bookings_dds_design.md +++ b/docs/design/bookings_dds_design.md @@ -168,13 +168,14 @@ DISTRIBUTED BY (ticket_no) ### 3.8. Политика NULL FK в факте -FK суррогатные ключи разделены на три группы: +FK суррогатные ключи разделены на четыре группы по источнику lookup: -| Группа | FK | NULL допустим? | Причина | -|--------|-----|---------------|---------| -| **Обязательные** | `tariff_sk`, `passenger_sk` | Нет | Данные всегда есть в ODS (segments, tickets). NULL = баг загрузки. | -| **Зависят от маршрута** | `route_sk`, `departure_airport_sk`, `arrival_airport_sk`, `airplane_sk` | Нет (в норме) | Маршрут должен быть в ODS. NULL = аномалия данных, DQ предупреждает. | -| **Зависят от расписания** | `calendar_sk` | Допустим (редко) | `scheduled_departure` может быть NULL в ODS. DQ считает и логирует, но не фейлит. | +| Группа | FK | Lookup-путь | NULL допустим? | Причина | +|--------|-----|-------------|---------------|---------| +| **Обязательные** | `tariff_sk`, `passenger_sk` | `dim_tariffs` / `dim_passengers` (SCD1) | Нет | Данные всегда есть в ODS. NULL = баг загрузки. | +| **Эталонный справочник** | `departure_airport_sk`, `arrival_airport_sk` | `ods.routes` → `dim_airports` | Нет (в норме) | Аэропорты разрешаются через `ods.routes` (эталон), не через `dim_routes`. NULL = аномалия данных, DQ проверяет с порогом 1%. | +| **Point-in-time SCD2** | `route_sk`, `airplane_sk` | `dim_routes` (SCD2 lookup) | Нет (в норме) | Point-in-time привязка к версии маршрута на дату рейса. NULL = аномалия, DQ предупреждает. | +| **Расписание** | `calendar_sk` | `dim_calendar` | Допустим (редко) | `scheduled_departure` может быть NULL в ODS. DQ считает и логирует. | DQ-проверки явно контролируют каждую группу (см. секцию 6). @@ -459,15 +460,27 @@ WITH fact_src AS ( JOIN ods.tickets AS tkt ON tkt.ticket_no = seg.ticket_no JOIN ods.bookings AS bkg ON bkg.book_ref = tkt.book_ref JOIN ods.flights AS flt ON flt.flight_id = seg.flight_id + -- Airport lookup через ods.routes (эталонный справочник). + -- Аэропорты вылета/прилёта одинаковы во всех версиях маршрута — + -- безопасно брать из ODS без point-in-time логики. + LEFT JOIN ( + SELECT route_no, departure_airport, arrival_airport + FROM ( + SELECT route_no, departure_airport, arrival_airport, + ROW_NUMBER() OVER (PARTITION BY route_no ORDER BY validity DESC) AS rn + FROM ods.routes + ) ranked + WHERE rn = 1 + ) AS ods_rte ON ods_rte.route_no = flt.route_no -- SCD2 point-in-time: версия маршрута, актуальная на дату вылета LEFT JOIN dds.dim_routes AS rte ON rte.route_bk = flt.route_no AND flt.scheduled_departure::DATE >= rte.valid_from AND (rte.valid_to IS NULL OR flt.scheduled_departure::DATE < rte.valid_to) LEFT JOIN dds.dim_calendar AS cal ON cal.date_actual = flt.scheduled_departure::DATE - LEFT JOIN dds.dim_airports AS dep ON dep.airport_bk = rte.departure_airport - LEFT JOIN dds.dim_airports AS arr ON arr.airport_bk = rte.arrival_airport - LEFT JOIN dds.dim_airplanes AS ap ON ap.airplane_bk = rte.airplane_code + LEFT JOIN dds.dim_airports AS dep ON dep.airport_bk = ods_rte.departure_airport -- через ods.routes + LEFT JOIN dds.dim_airports AS arr ON arr.airport_bk = ods_rte.arrival_airport -- через ods.routes + LEFT JOIN dds.dim_airplanes AS ap ON ap.airplane_bk = rte.airplane_code -- через dim_routes LEFT JOIN dds.dim_tariffs AS tar ON tar.fare_conditions = seg.fare_conditions LEFT JOIN dds.dim_passengers AS pax ON pax.passenger_bk = tkt.passenger_id LEFT JOIN ods.boarding_passes AS bp @@ -497,8 +510,9 @@ ANALYZE dds.fact_flight_sales; ### 5.5. Модель историчности факта Dimension SK фиксируются **при INSERT** и не перезаписываются: -- `route_sk` — версия маршрута на дату `scheduled_departure` (point-in-time SCD2 lookup); -- `departure_airport_sk`, `arrival_airport_sk`, `airplane_sk` — из той же версии маршрута; +- `departure_airport_sk`, `arrival_airport_sk` — из `ods.routes` → `dim_airports` (эталонный справочник; аэропорты одинаковы для всех версий маршрута); +- `route_sk` — версия маршрута на дату `scheduled_departure` (point-in-time SCD2 lookup через `dim_routes`); +- `airplane_sk` — из той же point-in-time версии `dim_routes`; - `calendar_sk`, `tariff_sk`, `passenger_sk` — из текущих SCD1-измерений на момент INSERT. UPDATE факта обновляет только **мутабельные поля**: `is_boarded`, `seat_no`, `price` diff --git a/docs/design/db_schema.md b/docs/design/db_schema.md index 400290a..9e3d34d 100644 --- a/docs/design/db_schema.md +++ b/docs/design/db_schema.md @@ -115,6 +115,7 @@ graph LR 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 @@ -220,12 +221,12 @@ SK генерация: `MAX(sk) + ROW_NUMBER()` (безопасно при `conc `dds.fact_flight_sales` — зерно: 1 строка = 1 сегмент билета (`ticket_no` + `flight_id`). -| FK | Источник | Примечание | -|----|----------|------------| +| FK | Lookup-путь | Примечание | +|----|-------------|------------| | `calendar_sk` | `dim_calendar` | Дата вылета | -| `departure_airport_sk` | `dim_airports` | Аэропорт вылета | -| `arrival_airport_sk` | `dim_airports` | Аэропорт прилёта | -| `airplane_sk` | `dim_airplanes` | Самолёт | +| `departure_airport_sk` | `ods.routes` → `dim_airports` | Аэропорт вылета (эталонный справочник) | +| `arrival_airport_sk` | `ods.routes` → `dim_airports` | Аэропорт прилёта (эталонный справочник) | +| `airplane_sk` | `dim_routes` → `dim_airplanes` | Самолёт (point-in-time SCD2) | | `tariff_sk` | `dim_tariffs` | Тариф | | `passenger_sk` | `dim_passengers` | Пассажир | | `route_sk` | `dim_routes` | Версия маршрута (SCD2, point-in-time) |