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:
@@ -7,8 +7,7 @@ from __future__ import annotations
|
|||||||
Создаёт 5 DM-витрин: sales_report, route_performance, passenger_loyalty,
|
Создаёт 5 DM-витрин: sales_report, route_performance, passenger_loyalty,
|
||||||
airport_traffic, monthly_overview.
|
airport_traffic, monthly_overview.
|
||||||
|
|
||||||
На данном этапе реализованы витрины: sales_report, route_performance, passenger_loyalty, airport_traffic.
|
На данном этапе реализованы все 5 витрин: sales_report, route_performance, passenger_loyalty, airport_traffic, monthly_overview.
|
||||||
Остальные витрины будут добавлены в последующих этапах.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -60,8 +59,12 @@ with DAG(
|
|||||||
sql="dm/airport_traffic_ddl.sql",
|
sql="dm/airport_traffic_ddl.sql",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Заглушки для будущих витрин (будут реализованы в этапе 5)
|
# Витрина: Помесячная сводка (Этап 5)
|
||||||
# apply_dm_monthly_overview_ddl = PostgresOperator(...)
|
apply_dm_monthly_overview_ddl = PostgresOperator(
|
||||||
|
task_id="apply_dm_monthly_overview_ddl",
|
||||||
|
postgres_conn_id=GREENPLUM_CONN_ID,
|
||||||
|
sql="dm/monthly_overview_ddl.sql",
|
||||||
|
)
|
||||||
|
|
||||||
# Линейная цепочка
|
# Линейная цепочка
|
||||||
apply_dm_sales_report_ddl >> apply_dm_route_performance_ddl >> apply_dm_passenger_loyalty_ddl >> apply_dm_airport_traffic_ddl
|
apply_dm_sales_report_ddl >> apply_dm_route_performance_ddl >> apply_dm_passenger_loyalty_ddl >> apply_dm_airport_traffic_ddl >> apply_dm_monthly_overview_ddl
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ from __future__ import annotations
|
|||||||
- все витрины загружаются параллельно (не зависят друг от друга);
|
- все витрины загружаются параллельно (не зависят друг от друга);
|
||||||
- sales_report использует UPSERT (heap-таблица), route_performance — Full Rebuild (AO Column).
|
- sales_report использует UPSERT (heap-таблица), route_performance — Full Rebuild (AO Column).
|
||||||
|
|
||||||
На данном этапе реализованы витрины: sales_report, route_performance, passenger_loyalty, airport_traffic.
|
На данном этапе реализованы все 5 витрин: sales_report, route_performance, passenger_loyalty, airport_traffic, monthly_overview.
|
||||||
Остальные витрины будут добавлены в последующих этапах.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -107,9 +106,18 @@ with DAG(
|
|||||||
sql="dm/airport_traffic_dq.sql",
|
sql="dm/airport_traffic_dq.sql",
|
||||||
)
|
)
|
||||||
|
|
||||||
# === Заглушки для будущих витрин (будут реализованы в этапе 5) ===
|
# === Витрина: Помесячная сводка (Этап 5) ===
|
||||||
# load_dm_monthly_overview = PostgresOperator(...)
|
load_dm_monthly_overview = PostgresOperator(
|
||||||
# dq_dm_monthly_overview = PostgresOperator(...)
|
task_id="load_dm_monthly_overview",
|
||||||
|
postgres_conn_id=GREENPLUM_CONN_ID,
|
||||||
|
sql="dm/monthly_overview_load.sql",
|
||||||
|
)
|
||||||
|
|
||||||
|
dq_dm_monthly_overview = PostgresOperator(
|
||||||
|
task_id="dq_dm_monthly_overview",
|
||||||
|
postgres_conn_id=GREENPLUM_CONN_ID,
|
||||||
|
sql="dm/monthly_overview_dq.sql",
|
||||||
|
)
|
||||||
|
|
||||||
# finish-задача
|
# finish-задача
|
||||||
finish_dm_summary = PythonOperator(
|
finish_dm_summary = PythonOperator(
|
||||||
@@ -122,7 +130,8 @@ with DAG(
|
|||||||
load_dm_sales_report,
|
load_dm_sales_report,
|
||||||
load_dm_route_performance,
|
load_dm_route_performance,
|
||||||
load_dm_passenger_loyalty,
|
load_dm_passenger_loyalty,
|
||||||
load_dm_airport_traffic
|
load_dm_airport_traffic,
|
||||||
|
load_dm_monthly_overview
|
||||||
]
|
]
|
||||||
|
|
||||||
# Связываем dq с finish
|
# Связываем dq с finish
|
||||||
@@ -130,3 +139,4 @@ with DAG(
|
|||||||
load_dm_route_performance >> dq_dm_route_performance >> finish_dm_summary
|
load_dm_route_performance >> dq_dm_route_performance >> finish_dm_summary
|
||||||
load_dm_passenger_loyalty >> dq_dm_passenger_loyalty >> finish_dm_summary
|
load_dm_passenger_loyalty >> dq_dm_passenger_loyalty >> finish_dm_summary
|
||||||
load_dm_airport_traffic >> dq_dm_airport_traffic >> finish_dm_summary
|
load_dm_airport_traffic >> dq_dm_airport_traffic >> finish_dm_summary
|
||||||
|
load_dm_monthly_overview >> dq_dm_monthly_overview >> finish_dm_summary
|
||||||
|
|||||||
+13
-2
@@ -93,12 +93,23 @@ trigger_and_wait "bookings_to_gp_dds"
|
|||||||
trigger_and_wait "bookings_to_gp_dm"
|
trigger_and_wait "bookings_to_gp_dm"
|
||||||
|
|
||||||
warn "=== Верификация данных ==="
|
warn "=== Верификация данных ==="
|
||||||
# Выполняем простые проверки строк, чтобы убедиться, что данные дошли до витрины
|
# Выполняем проверки строк и бизнес-логики, чтобы убедиться, что все витрины DM-слоя заполнены
|
||||||
docker compose -f docker-compose.yml exec greenplum bash -c "su - gpadmin -c \"psql -d gp_dwh -c \\\"
|
docker compose -f docker-compose.yml exec greenplum bash -c "su - gpadmin -c \"psql -d gp_dwh -c \\\"
|
||||||
SELECT 'STG bookings' as layer, COUNT(*) FROM stg.bookings UNION ALL
|
SELECT 'STG bookings' as layer, COUNT(*) FROM stg.bookings UNION ALL
|
||||||
SELECT 'ODS bookings', COUNT(*) FROM ods.bookings UNION ALL
|
SELECT 'ODS bookings', COUNT(*) FROM ods.bookings UNION ALL
|
||||||
SELECT 'DDS fact', COUNT(*) FROM dds.fact_flight_sales UNION ALL
|
SELECT 'DDS fact', COUNT(*) FROM dds.fact_flight_sales UNION ALL
|
||||||
SELECT 'DM report', COUNT(*) FROM dm.sales_report;
|
SELECT 'DM sales_report', COUNT(*) FROM dm.sales_report UNION ALL
|
||||||
|
SELECT 'DM route_performance', COUNT(*) FROM dm.route_performance UNION ALL
|
||||||
|
SELECT 'DM passenger_loyalty', COUNT(*) FROM dm.passenger_loyalty UNION ALL
|
||||||
|
SELECT 'DM airport_traffic', COUNT(*) FROM dm.airport_traffic UNION ALL
|
||||||
|
SELECT 'DM monthly_overview', COUNT(*) FROM dm.monthly_overview;
|
||||||
|
|
||||||
|
-- Дополнительная проверка бизнес-логики (load factor не должен быть NULL и должен быть в пределах разумного)
|
||||||
|
SELECT
|
||||||
|
'Check LF' as check,
|
||||||
|
COUNT(*) as total_rows,
|
||||||
|
SUM(CASE WHEN avg_load_factor IS NOT NULL AND avg_load_factor >= 0 THEN 1 ELSE 0 END) as valid_lf
|
||||||
|
FROM dm.monthly_overview;
|
||||||
\\\"\""
|
\\\"\""
|
||||||
|
|
||||||
warn "E2E ETL тест успешно завершен!"
|
warn "E2E ETL тест успешно завершен!"
|
||||||
|
|||||||
@@ -60,3 +60,7 @@ FORMAT 'CUSTOM' (formatter='pxfwritable_import');
|
|||||||
|
|
||||||
-- DDL для DM-слоя (Data Mart).
|
-- DDL для DM-слоя (Data Mart).
|
||||||
\i dm/sales_report_ddl.sql
|
\i dm/sales_report_ddl.sql
|
||||||
|
\i dm/route_performance_ddl.sql
|
||||||
|
\i dm/passenger_loyalty_ddl.sql
|
||||||
|
\i dm/airport_traffic_ddl.sql
|
||||||
|
\i dm/monthly_overview_ddl.sql
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- DDL для витрины dm.monthly_overview (Помесячная сводка).
|
||||||
|
--
|
||||||
|
-- Учебные цели:
|
||||||
|
-- 1. Двухуровневая агрегация (до рейса, затем до месяца) для точного расчета
|
||||||
|
-- средних показателей (load factor), избегая парадокса Симпсона.
|
||||||
|
-- 2. Выбор ключа распределения (Distribution Key):
|
||||||
|
-- Мы используем airplane_sk, а НЕ (year, month). Распределение по датам в MPP —
|
||||||
|
-- это антипаттерн, приводящий к Data Skew (весь месяц на одном сегменте).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm.monthly_overview (
|
||||||
|
-- Зерно: Месяц и самолет
|
||||||
|
year_actual INTEGER NOT NULL,
|
||||||
|
month_actual INTEGER NOT NULL,
|
||||||
|
airplane_sk INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- Денормализованные атрибуты
|
||||||
|
airplane_bk TEXT NOT NULL,
|
||||||
|
airplane_model TEXT NOT NULL,
|
||||||
|
total_seats INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- Метрики
|
||||||
|
total_flights INTEGER NOT NULL DEFAULT 0,
|
||||||
|
total_tickets INTEGER NOT NULL DEFAULT 0,
|
||||||
|
total_boarded INTEGER NOT NULL DEFAULT 0,
|
||||||
|
total_revenue NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
avg_ticket_price NUMERIC(10,2),
|
||||||
|
avg_load_factor NUMERIC(5,4), -- Точный load factor (сначала по рейсу, потом среднее)
|
||||||
|
unique_routes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
unique_passengers INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Служебные
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
_load_id TEXT NOT NULL,
|
||||||
|
_load_ts TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
)
|
||||||
|
WITH (appendonly=false) -- Heap для UPSERT
|
||||||
|
DISTRIBUTED BY (airplane_sk); -- Избегаем распределения по (year, month)
|
||||||
|
|
||||||
|
COMMENT ON TABLE dm.monthly_overview IS 'Витрина: помесячная аналитика с точным load_factor (Двухуровневая агрегация, Heap)';
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
-- DQ проверки для витрины dm.monthly_overview.
|
||||||
|
--
|
||||||
|
-- Учебные цели:
|
||||||
|
-- 1. Валидация логики двухуровневой агрегации: load factor не должен превышать 1.0
|
||||||
|
-- (в отличие от route_performance, где допускалась погрешность из-за упрощенного расчета).
|
||||||
|
-- 2. Базовые санити-проверки для дат (month BETWEEN 1 AND 12).
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
row_count INTEGER;
|
||||||
|
duplicate_count INTEGER;
|
||||||
|
invalid_month_count INTEGER;
|
||||||
|
invalid_load_factor_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- 1. Проверка на наполненность
|
||||||
|
SELECT COUNT(*) INTO row_count FROM dm.monthly_overview;
|
||||||
|
IF row_count = 0 THEN
|
||||||
|
RAISE EXCEPTION 'DQ Error: Таблица dm.monthly_overview пуста.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 2. Проверка на уникальность (зерно - год + месяц + самолет)
|
||||||
|
SELECT COUNT(*) INTO duplicate_count
|
||||||
|
FROM (
|
||||||
|
SELECT year_actual, month_actual, airplane_sk FROM dm.monthly_overview
|
||||||
|
GROUP BY year_actual, month_actual, airplane_sk HAVING COUNT(*) > 1
|
||||||
|
) q;
|
||||||
|
IF duplicate_count > 0 THEN
|
||||||
|
RAISE EXCEPTION 'DQ Error: В dm.monthly_overview обнаружены дубликаты по (year, month, airplane_sk) (% шт).', duplicate_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 3. Санити-проверка месяца
|
||||||
|
SELECT COUNT(*) INTO invalid_month_count
|
||||||
|
FROM dm.monthly_overview
|
||||||
|
WHERE month_actual < 1 OR month_actual > 12;
|
||||||
|
|
||||||
|
IF invalid_month_count > 0 THEN
|
||||||
|
RAISE EXCEPTION 'DQ Error: В dm.monthly_overview обнаружены некорректные месяцы (% строк).', invalid_month_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 4. Проверка точного load_factor
|
||||||
|
-- В этой витрине мы считаем его честно (по рейсам), поэтому он строго <= 1.0
|
||||||
|
-- (исключая экзотические случаи овербукинга, но для учебного стенда ставим жесткий лимит 1.0).
|
||||||
|
SELECT COUNT(*) INTO invalid_load_factor_count
|
||||||
|
FROM dm.monthly_overview
|
||||||
|
WHERE avg_load_factor < 0 OR avg_load_factor > 1.0;
|
||||||
|
|
||||||
|
IF invalid_load_factor_count > 0 THEN
|
||||||
|
RAISE EXCEPTION 'DQ Error: В dm.monthly_overview обнаружен avg_load_factor вне диапазона 0..1 (% строк).', invalid_load_factor_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE 'DQ Success: dm.monthly_overview успешно прошла все проверки (% строк).', row_count;
|
||||||
|
END $$;
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -29,7 +29,7 @@ JOIN dds.dim_routes r ON f.route_sk = r.route_sk
|
|||||||
JOIN dds.dim_calendar cal ON f.calendar_sk = cal.calendar_sk
|
JOIN dds.dim_calendar cal ON f.calendar_sk = cal.calendar_sk
|
||||||
GROUP BY r.route_bk;
|
GROUP BY r.route_bk;
|
||||||
|
|
||||||
-- Шаг 3: Объединяем агрегаты с АКТУАЛЬНЫМИ атрибутами маршрута.
|
-- Шаг 3: Объединяем агрегаты с АКТУАЛЬНЫМИ атрибутами маршрута, аэропортов и самолетов.
|
||||||
INSERT INTO dm.route_performance (
|
INSERT INTO dm.route_performance (
|
||||||
route_bk,
|
route_bk,
|
||||||
route_sk,
|
route_sk,
|
||||||
@@ -54,13 +54,13 @@ INSERT INTO dm.route_performance (
|
|||||||
SELECT
|
SELECT
|
||||||
m.route_bk,
|
m.route_bk,
|
||||||
r_curr.route_sk,
|
r_curr.route_sk,
|
||||||
r_curr.departure_airport_bk,
|
dep.airport_bk AS departure_airport_bk,
|
||||||
r_curr.departure_city,
|
dep.city AS departure_city,
|
||||||
r_curr.arrival_airport_bk,
|
arr.airport_bk AS arrival_airport_bk,
|
||||||
r_curr.arrival_city,
|
arr.city AS arrival_city,
|
||||||
r_curr.airplane_bk,
|
air.airplane_bk,
|
||||||
r_curr.airplane_model,
|
air.model AS airplane_model,
|
||||||
r_curr.total_seats,
|
air.total_seats,
|
||||||
m.total_flights,
|
m.total_flights,
|
||||||
m.total_tickets,
|
m.total_tickets,
|
||||||
m.total_boarded,
|
m.total_boarded,
|
||||||
@@ -69,14 +69,15 @@ SELECT
|
|||||||
(m.total_revenue / NULLIF(m.total_tickets, 0))::NUMERIC(10,2),
|
(m.total_revenue / NULLIF(m.total_tickets, 0))::NUMERIC(10,2),
|
||||||
m.avg_boarding_rate::NUMERIC(5,4),
|
m.avg_boarding_rate::NUMERIC(5,4),
|
||||||
-- Load Factor: общее кол-во посадок / (кол-во рейсов * мест в самолете)
|
-- Load Factor: общее кол-во посадок / (кол-во рейсов * мест в самолете)
|
||||||
-- Это учебное допущение (упрощение), т.к. самолет мог меняться.
|
|
||||||
ROUND(
|
ROUND(
|
||||||
m.total_boarded::NUMERIC / NULLIF(m.total_flights * r_curr.total_seats, 0),
|
m.total_boarded::NUMERIC / NULLIF(m.total_flights * air.total_seats, 0),
|
||||||
4
|
4
|
||||||
) AS avg_load_factor,
|
) AS avg_load_factor,
|
||||||
m.first_flight_date,
|
m.first_flight_date,
|
||||||
m.last_flight_date,
|
m.last_flight_date,
|
||||||
'{{ run_id }}' AS _load_id
|
'{{ run_id }}' AS _load_id
|
||||||
FROM tmp_route_metrics m
|
FROM tmp_route_metrics m
|
||||||
JOIN dds.dim_routes r_curr ON m.route_bk = r_curr.route_bk
|
JOIN dds.dim_routes r_curr ON m.route_bk = r_curr.route_bk AND r_curr.valid_to IS NULL
|
||||||
WHERE r_curr.valid_to IS NULL; -- Берем только АКТУАЛЬНУЮ версию для денормализации
|
JOIN dds.dim_airports dep ON r_curr.departure_airport = dep.airport_bk
|
||||||
|
JOIN dds.dim_airports arr ON r_curr.arrival_airport = arr.airport_bk
|
||||||
|
JOIN dds.dim_airplanes air ON r_curr.airplane_code = air.airplane_bk;
|
||||||
|
|||||||
@@ -390,11 +390,18 @@ def test_bookings_dm_ddl_dag_structure():
|
|||||||
|
|
||||||
expected_tasks = {
|
expected_tasks = {
|
||||||
"apply_dm_sales_report_ddl",
|
"apply_dm_sales_report_ddl",
|
||||||
|
"apply_dm_route_performance_ddl",
|
||||||
|
"apply_dm_passenger_loyalty_ddl",
|
||||||
|
"apply_dm_airport_traffic_ddl",
|
||||||
|
"apply_dm_monthly_overview_ddl",
|
||||||
}
|
}
|
||||||
assert expected_tasks.issubset(dag.task_dict.keys())
|
assert expected_tasks.issubset(dag.task_dict.keys())
|
||||||
|
|
||||||
# На данном этапе только одна витрина
|
# Проверяем линейную цепочку
|
||||||
# В будущем: линейная цепочка из 5 задач
|
_assert_direct_edge(dag, "apply_dm_sales_report_ddl", "apply_dm_route_performance_ddl")
|
||||||
|
_assert_direct_edge(dag, "apply_dm_route_performance_ddl", "apply_dm_passenger_loyalty_ddl")
|
||||||
|
_assert_direct_edge(dag, "apply_dm_passenger_loyalty_ddl", "apply_dm_airport_traffic_ddl")
|
||||||
|
_assert_direct_edge(dag, "apply_dm_airport_traffic_ddl", "apply_dm_monthly_overview_ddl")
|
||||||
|
|
||||||
|
|
||||||
def test_bookings_to_gp_dm_dag_structure():
|
def test_bookings_to_gp_dm_dag_structure():
|
||||||
@@ -405,13 +412,30 @@ def test_bookings_to_gp_dm_dag_structure():
|
|||||||
"start_dm",
|
"start_dm",
|
||||||
"load_dm_sales_report",
|
"load_dm_sales_report",
|
||||||
"dq_dm_sales_report",
|
"dq_dm_sales_report",
|
||||||
|
"load_dm_route_performance",
|
||||||
|
"dq_dm_route_performance",
|
||||||
|
"load_dm_passenger_loyalty",
|
||||||
|
"dq_dm_passenger_loyalty",
|
||||||
|
"load_dm_airport_traffic",
|
||||||
|
"dq_dm_airport_traffic",
|
||||||
|
"load_dm_monthly_overview",
|
||||||
|
"dq_dm_monthly_overview",
|
||||||
"finish_dm_summary",
|
"finish_dm_summary",
|
||||||
}
|
}
|
||||||
assert expected_tasks.issubset(dag.task_dict.keys())
|
assert expected_tasks.issubset(dag.task_dict.keys())
|
||||||
|
|
||||||
# Проверяем зависимости load -> dq
|
marts = [
|
||||||
_assert_direct_edge(dag, "load_dm_sales_report", "dq_dm_sales_report")
|
"sales_report",
|
||||||
|
"route_performance",
|
||||||
|
"passenger_loyalty",
|
||||||
|
"airport_traffic",
|
||||||
|
"monthly_overview",
|
||||||
|
]
|
||||||
|
|
||||||
# Проверяем, что start -> load -> dq -> finish
|
for mart in marts:
|
||||||
_assert_reachable(dag, "start_dm", "load_dm_sales_report")
|
# load -> dq
|
||||||
_assert_reachable(dag, "dq_dm_sales_report", "finish_dm_summary")
|
_assert_direct_edge(dag, f"load_dm_{mart}", f"dq_dm_{mart}")
|
||||||
|
# start -> load
|
||||||
|
_assert_reachable(dag, "start_dm", f"load_dm_{mart}")
|
||||||
|
# dq -> finish
|
||||||
|
_assert_reachable(dag, f"dq_dm_{mart}", "finish_dm_summary")
|
||||||
|
|||||||
Reference in New Issue
Block a user