feat(dm): завершено построение DM-слоя (5 витрин) и исправлены баги денормализации
- Зачем: - предоставить студентам полный набор аналитических витрин с примерами различных паттернов (UPSERT, Full Rebuild, UNION ALL, двухуровневая агрегация). - Что: - реализованы витрины: sales_report, route_performance, passenger_loyalty, airport_traffic, monthly_overview. - исправлен баг в route_performance_load.sql: добавлены JOIN к dim_airports и dim_airplanes для корректной денормализации атрибутов. - обновлен скрипт e2e_etl.sh: добавлена верификация всех 5 витрин и проверка бизнес-логики (load factor). - обновлены DAGи, тесты и главный DDL скрипт. - Проверка: - автоматизированный прогон e2e_etl.sh через REST API Airflow.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
-- Загрузка витрины dm.monthly_overview: инкрементальный UPSERT по месяцам.
|
||||
--
|
||||
-- Учебные цели:
|
||||
-- 1. Двухуровневая агрегация: чтобы честно посчитать среднюю заполняемость (avg_load_factor),
|
||||
-- мы сначала считаем её для КАЖДОГО рейса, а затем берем среднее по месяцу.
|
||||
-- 2. Ограничения SCD1: dim_airplanes — это SCD1-измерение. Мы берем total_seats из
|
||||
-- его текущего состояния. Если бы самолет переоборудовали (изменили число мест) в прошлом,
|
||||
-- для точного исторического расчета нам потребовалось бы SCD2-измерение.
|
||||
|
||||
-- Шаг 1: Находим затронутые месяцы (HWM).
|
||||
CREATE TEMP TABLE tmp_monthly_affected_dates ON COMMIT DROP AS
|
||||
SELECT DISTINCT cal.year_actual, cal.month_actual
|
||||
FROM dds.fact_flight_sales f
|
||||
JOIN dds.dim_calendar cal ON f.calendar_sk = cal.calendar_sk
|
||||
WHERE f._load_ts > (
|
||||
SELECT COALESCE(MAX(_load_ts), '1900-01-01'::TIMESTAMP)
|
||||
FROM dm.monthly_overview
|
||||
);
|
||||
|
||||
-- Шаг 2: Уровень 1 - Агрегация фактов до рейса (flight_id).
|
||||
-- Считаем точный load_factor для каждого отдельного перелета.
|
||||
CREATE TEMP TABLE tmp_flight_level_metrics ON COMMIT DROP AS
|
||||
SELECT
|
||||
cal.year_actual,
|
||||
cal.month_actual,
|
||||
f.flight_id,
|
||||
f.airplane_sk,
|
||||
a.total_seats, -- ВНИМАНИЕ: берется текущее значение (SCD1)
|
||||
COUNT(*) AS tickets_sold_per_flight,
|
||||
SUM(CASE WHEN f.is_boarded THEN 1 ELSE 0 END) AS boarded_per_flight,
|
||||
SUM(f.price) AS revenue_per_flight,
|
||||
-- Точный load factor конкретного рейса: посаженные пассажиры / кол-во мест
|
||||
(SUM(CASE WHEN f.is_boarded THEN 1 ELSE 0 END)::NUMERIC / NULLIF(a.total_seats, 0)) AS flight_load_factor
|
||||
FROM dds.fact_flight_sales f
|
||||
JOIN dds.dim_calendar cal ON f.calendar_sk = cal.calendar_sk
|
||||
JOIN dds.dim_airplanes a ON f.airplane_sk = a.airplane_sk
|
||||
-- Фильтруем только затронутые месяцы
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM tmp_monthly_affected_dates tad
|
||||
WHERE tad.year_actual = cal.year_actual AND tad.month_actual = cal.month_actual
|
||||
)
|
||||
GROUP BY cal.year_actual, cal.month_actual, f.flight_id, f.airplane_sk, a.total_seats;
|
||||
|
||||
-- Шаг 3: Уровень 2 - Агрегация от рейсов до месяцев.
|
||||
-- Плюс собираем уникальные метрики (пассажиры, маршруты) напрямую из фактов.
|
||||
CREATE TEMP TABLE tmp_monthly_overview_delta ON COMMIT DROP AS
|
||||
WITH flight_aggs AS (
|
||||
SELECT
|
||||
year_actual,
|
||||
month_actual,
|
||||
airplane_sk,
|
||||
COUNT(DISTINCT flight_id) AS total_flights,
|
||||
SUM(tickets_sold_per_flight) AS total_tickets,
|
||||
SUM(boarded_per_flight) AS total_boarded,
|
||||
SUM(revenue_per_flight) AS total_revenue,
|
||||
-- Честное среднее от рейсовых показателей
|
||||
AVG(flight_load_factor) AS avg_load_factor
|
||||
FROM tmp_flight_level_metrics
|
||||
GROUP BY year_actual, month_actual, airplane_sk
|
||||
),
|
||||
unique_aggs AS (
|
||||
SELECT
|
||||
cal.year_actual,
|
||||
cal.month_actual,
|
||||
f.airplane_sk,
|
||||
COUNT(DISTINCT r.route_bk) AS unique_routes, -- Считаем по BK (SCD2)
|
||||
COUNT(DISTINCT f.passenger_sk) AS unique_passengers
|
||||
FROM dds.fact_flight_sales f
|
||||
JOIN dds.dim_calendar cal ON f.calendar_sk = cal.calendar_sk
|
||||
JOIN dds.dim_routes r ON f.route_sk = r.route_sk
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM tmp_monthly_affected_dates tad
|
||||
WHERE tad.year_actual = cal.year_actual AND tad.month_actual = cal.month_actual
|
||||
)
|
||||
GROUP BY cal.year_actual, cal.month_actual, f.airplane_sk
|
||||
)
|
||||
SELECT
|
||||
fa.year_actual,
|
||||
fa.month_actual,
|
||||
fa.airplane_sk,
|
||||
a.airplane_bk,
|
||||
a.model AS airplane_model,
|
||||
a.total_seats,
|
||||
fa.total_flights,
|
||||
fa.total_tickets,
|
||||
fa.total_boarded,
|
||||
fa.total_revenue,
|
||||
(fa.total_revenue / NULLIF(fa.total_tickets, 0))::NUMERIC(10,2) AS avg_ticket_price,
|
||||
fa.avg_load_factor::NUMERIC(5,4) AS avg_load_factor,
|
||||
ua.unique_routes,
|
||||
ua.unique_passengers
|
||||
FROM flight_aggs fa
|
||||
JOIN unique_aggs ua ON fa.year_actual = ua.year_actual AND fa.month_actual = ua.month_actual AND fa.airplane_sk = ua.airplane_sk
|
||||
JOIN dds.dim_airplanes a ON fa.airplane_sk = a.airplane_sk;
|
||||
|
||||
-- Шаг 4: UPDATE существующих записей.
|
||||
UPDATE dm.monthly_overview AS tgt
|
||||
SET
|
||||
airplane_bk = src.airplane_bk,
|
||||
airplane_model = src.airplane_model,
|
||||
total_seats = src.total_seats,
|
||||
total_flights = src.total_flights,
|
||||
total_tickets = src.total_tickets,
|
||||
total_boarded = src.total_boarded,
|
||||
total_revenue = src.total_revenue,
|
||||
avg_ticket_price = src.avg_ticket_price,
|
||||
avg_load_factor = src.avg_load_factor,
|
||||
unique_routes = src.unique_routes,
|
||||
unique_passengers = src.unique_passengers,
|
||||
updated_at = now(),
|
||||
_load_id = '{{ run_id }}',
|
||||
_load_ts = now()
|
||||
FROM tmp_monthly_overview_delta AS src
|
||||
WHERE tgt.year_actual = src.year_actual
|
||||
AND tgt.month_actual = src.month_actual
|
||||
AND tgt.airplane_sk = src.airplane_sk
|
||||
AND (
|
||||
tgt.total_flights IS DISTINCT FROM src.total_flights
|
||||
OR tgt.total_revenue IS DISTINCT FROM src.total_revenue
|
||||
OR tgt.total_boarded IS DISTINCT FROM src.total_boarded
|
||||
OR tgt.unique_passengers IS DISTINCT FROM src.unique_passengers
|
||||
OR tgt.airplane_model IS DISTINCT FROM src.airplane_model
|
||||
);
|
||||
|
||||
-- Шаг 5: INSERT новых записей.
|
||||
INSERT INTO dm.monthly_overview (
|
||||
year_actual,
|
||||
month_actual,
|
||||
airplane_sk,
|
||||
airplane_bk,
|
||||
airplane_model,
|
||||
total_seats,
|
||||
total_flights,
|
||||
total_tickets,
|
||||
total_boarded,
|
||||
total_revenue,
|
||||
avg_ticket_price,
|
||||
avg_load_factor,
|
||||
unique_routes,
|
||||
unique_passengers,
|
||||
_load_id
|
||||
)
|
||||
SELECT
|
||||
src.year_actual,
|
||||
src.month_actual,
|
||||
src.airplane_sk,
|
||||
src.airplane_bk,
|
||||
src.airplane_model,
|
||||
src.total_seats,
|
||||
src.total_flights,
|
||||
src.total_tickets,
|
||||
src.total_boarded,
|
||||
src.total_revenue,
|
||||
src.avg_ticket_price,
|
||||
src.avg_load_factor,
|
||||
src.unique_routes,
|
||||
src.unique_passengers,
|
||||
'{{ run_id }}' AS _load_id
|
||||
FROM tmp_monthly_overview_delta AS src
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM dm.monthly_overview AS tgt
|
||||
WHERE tgt.year_actual = src.year_actual
|
||||
AND tgt.month_actual = src.month_actual
|
||||
AND tgt.airplane_sk = src.airplane_sk
|
||||
);
|
||||
Reference in New Issue
Block a user