- Зачем:
- fact_flight_sales брал airport_sk через dds.dim_routes (студенческое задание SCD2);
на ветке main без реализованного dim_routes все airport_sk были NULL,
и эталонные витрины sales_report/airport_traffic становились бесполезны.
- Что:
- fact_flight_sales_load.sql: добавлен subquery ods_rte (ROW_NUMBER по ods.routes),
dep/arr airport_sk теперь через ods.routes (эталон); airplane_sk — через dim_routes (point-in-time).
- analyst_spec.md: удалены STG-задания (1.1–1.3) и ods.routes (2.3), добавлены секции
«Почему STG уже реализован» и «Пересчёт факта», исправлен ODS intro, перенумерованы части.
- assignment_design.md: обновлены таблицы эталон/задание, порядок выполнения; убраны
validate_stg и check_ods_routes_rowcount из структуры валидационного DAG.
- db_schema.md: статус уточнён («реализовано в ветке solution»), добавлена оговорка
о заглушках на main, DQ-контракт дополнен примечанием.
- Проверка:
- make test → 4 passed, 9 skipped.
- ручная проверка: TRUNCATE dds.fact_flight_sales → перезапуск DDS DAG →
COUNT(departure_airport_sk) должен равняться COUNT(*).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
4.8 KiB
SQL
130 lines
4.8 KiB
SQL
-- Загрузка DDS fact_flight_sales: инкрементальный UPSERT по (ticket_no, flight_id).
|
|
|
|
-- Statement 1: UPDATE существующих строк факта.
|
|
-- Обновляем только мутабельные поля; SK измерений не перезаписываем.
|
|
UPDATE dds.fact_flight_sales AS f
|
|
SET seat_no = bp.seat_no,
|
|
price = seg.amount,
|
|
is_boarded = (bp.ticket_no IS NOT NULL),
|
|
_load_id = '{{ run_id }}',
|
|
_load_ts = now()
|
|
FROM ods.segments AS seg
|
|
LEFT JOIN ods.boarding_passes AS bp
|
|
ON bp.ticket_no = seg.ticket_no
|
|
AND bp.flight_id = seg.flight_id
|
|
WHERE f.ticket_no = seg.ticket_no
|
|
AND f.flight_id = seg.flight_id
|
|
AND (
|
|
f.is_boarded IS DISTINCT FROM (bp.ticket_no IS NOT NULL)
|
|
OR f.price IS DISTINCT FROM seg.amount
|
|
OR f.seat_no IS DISTINCT FROM bp.seat_no
|
|
);
|
|
|
|
-- Statement 2: INSERT новых строк факта.
|
|
-- Dimension SK фиксируются на момент вставки (point-in-time для SCD2 routes).
|
|
WITH fact_src AS (
|
|
SELECT
|
|
seg.ticket_no,
|
|
seg.flight_id,
|
|
cal.calendar_sk,
|
|
dep.airport_sk AS departure_airport_sk,
|
|
arr.airport_sk AS arrival_airport_sk,
|
|
ap.airplane_sk,
|
|
tar.tariff_sk,
|
|
pax.passenger_sk,
|
|
rte.route_sk,
|
|
tkt.book_ref,
|
|
bkg.book_date::DATE AS book_date,
|
|
bp.seat_no,
|
|
seg.amount AS price,
|
|
(bp.ticket_no IS NOT NULL) AS is_boarded
|
|
FROM ods.segments AS seg
|
|
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
|
|
-- Учебный комментарий: Late-arriving dimensions (Опаздывающие измерения)
|
|
-- Мы используем LEFT JOIN, так как факт (рейс/билет) может прийти раньше,
|
|
-- чем справочник (пассажир/маршрут) обновится в DDS.
|
|
--
|
|
-- Airport lookup через ods.routes (эталонный справочник), а не через dds.dim_routes
|
|
-- (студенческое задание SCD2). Это архитектурное решение: эталонный пайплайн работает
|
|
-- независимо от студенческого кода. Аэропорты вылета/прилёта одинаковы во всех
|
|
-- версиях одного route_no — безопасно брать из ODS без point-in-time логики.
|
|
-- ROW_NUMBER по validity DESC: выбираем актуальную версию расписания маршрута.
|
|
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
|
|
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 = 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 (point-in-time, как прежде)
|
|
LEFT JOIN dds.dim_tariffs AS tar
|
|
ON tar.fare_conditions = seg.fare_conditions
|
|
LEFT JOIN dds.dim_passengers AS pax
|
|
ON pax.passenger_id = tkt.passenger_id
|
|
LEFT JOIN ods.boarding_passes AS bp
|
|
ON bp.ticket_no = seg.ticket_no
|
|
AND bp.flight_id = seg.flight_id
|
|
)
|
|
INSERT INTO dds.fact_flight_sales (
|
|
calendar_sk,
|
|
departure_airport_sk,
|
|
arrival_airport_sk,
|
|
airplane_sk,
|
|
tariff_sk,
|
|
passenger_sk,
|
|
route_sk,
|
|
book_ref,
|
|
ticket_no,
|
|
flight_id,
|
|
book_date,
|
|
seat_no,
|
|
price,
|
|
is_boarded,
|
|
_load_id,
|
|
_load_ts
|
|
)
|
|
SELECT
|
|
s.calendar_sk,
|
|
s.departure_airport_sk,
|
|
s.arrival_airport_sk,
|
|
s.airplane_sk,
|
|
s.tariff_sk,
|
|
s.passenger_sk,
|
|
s.route_sk,
|
|
s.book_ref,
|
|
s.ticket_no,
|
|
s.flight_id,
|
|
s.book_date,
|
|
s.seat_no,
|
|
s.price,
|
|
s.is_boarded,
|
|
'{{ run_id }}',
|
|
now()
|
|
FROM fact_src AS s
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM dds.fact_flight_sales AS f
|
|
WHERE f.ticket_no = s.ticket_no
|
|
AND f.flight_id = s.flight_id
|
|
);
|
|
|
|
ANALYZE dds.fact_flight_sales;
|