- Зачем: - необходимо продемонстрировать студентам метод инкрементального пересчета "затронутых ключей" для больших справочных витрин. - Что: - созданы DDL, Load и DQ скрипты для витрины dm.passenger_loyalty (лояльность пассажиров). - реализован расчет моды (самый частый тариф) через PostgreSQL-специфику DISTINCT ON. - внедрена корректная агрегация SCD2-измерений (unique_routes) по бизнес-ключу route_bk. - настроено Heap-хранилище (WITH appendonly=false) для эффективного выполнения UPSERT. - обновлены DAGи bookings_dm_ddl и bookings_to_gp_dm для включения новой витрины в конвейер. - Проверка: - визуальный аудит SQL на предмет использования p.passenger_id (BK) и r.route_bk. - наличие учебной DQ-проверки ссылочной целостности и инварианта дат (first <= last). - проверка параллельности задач в Airflow DAG.
130 lines
5.9 KiB
SQL
130 lines
5.9 KiB
SQL
-- Загрузка витрины dm.passenger_loyalty: инкрементальный UPSERT по затронутым ключам.
|
|
--
|
|
-- Учебные цели:
|
|
-- 1. Метод "затронутых ключей": HWM по _load_ts находит затронутые ID пассажиров,
|
|
-- а затем мы ПЕРЕСЧИТЫВАЕМ всю историю именно для этого круга лиц.
|
|
-- Это гарантирует точность накопительных агрегатов (total_spent, dates).
|
|
-- 2. Использование DISTINCT ON (PostgreSQL-специфика): самый лаконичный способ
|
|
-- найти "самое частое" (моду) в рамках группы.
|
|
-- 3. Обработка NULL в фактах: фильтрация (passenger_sk IS NOT NULL).
|
|
-- 4. Агрегация SCD2-измерений: при подсчете уникальных маршрутов (dim_routes)
|
|
-- нужно агрегировать по BK (route_bk), т.к. один маршрут может иметь несколько SK (версий).
|
|
|
|
-- Шаг 1: Находим ID пассажиров, чьи данные изменились или добавились в фактах.
|
|
CREATE TEMP TABLE tmp_loyalty_affected_keys ON COMMIT DROP AS
|
|
SELECT DISTINCT passenger_sk
|
|
FROM dds.fact_flight_sales
|
|
WHERE _load_ts > (
|
|
SELECT COALESCE(MAX(_load_ts), '1900-01-01'::TIMESTAMP)
|
|
FROM dm.passenger_loyalty
|
|
)
|
|
AND passenger_sk IS NOT NULL;
|
|
|
|
-- Шаг 2: Для затронутых лиц считаем ИТОГОВЫЕ агрегаты по ВСЕЙ истории фактов.
|
|
CREATE TEMP TABLE tmp_passenger_delta ON COMMIT DROP AS
|
|
WITH base_metrics AS (
|
|
-- Агрегируем количественные метрики.
|
|
-- JOIN dds.dim_routes нужен для подсчета УНИКАЛЬНЫХ маршрутов по BK (т.к. dim_routes - SCD2).
|
|
SELECT
|
|
f.passenger_sk,
|
|
COUNT(DISTINCT f.book_ref) AS total_bookings,
|
|
COUNT(*) AS total_flights,
|
|
SUM(CASE WHEN f.is_boarded THEN 1 ELSE 0 END) AS total_boarded,
|
|
SUM(f.price) AS total_spent,
|
|
COUNT(DISTINCT r.route_bk) AS unique_routes, -- Агрегация по BK (бизнес-ключу) маршрута
|
|
MIN(cal.date_actual) AS first_flight_date,
|
|
MAX(cal.date_actual) AS last_flight_date
|
|
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 f.passenger_sk IN (SELECT passenger_sk FROM tmp_loyalty_affected_keys)
|
|
GROUP BY f.passenger_sk
|
|
),
|
|
fare_modes AS (
|
|
-- Находим самый частый класс обслуживания для каждого пассажира.
|
|
-- DISTINCT ON при ничьей выбирает произвольный вариант из топ-результатов.
|
|
SELECT DISTINCT ON (f.passenger_sk)
|
|
f.passenger_sk,
|
|
tar.fare_conditions AS favorite_fare_conditions
|
|
FROM dds.fact_flight_sales f
|
|
JOIN dds.dim_tariffs tar ON f.tariff_sk = tar.tariff_sk
|
|
WHERE f.passenger_sk IN (SELECT passenger_sk FROM tmp_loyalty_affected_keys)
|
|
GROUP BY f.passenger_sk, tar.fare_conditions
|
|
ORDER BY f.passenger_sk, COUNT(*) DESC
|
|
)
|
|
SELECT
|
|
bm.*,
|
|
fm.favorite_fare_conditions,
|
|
p.passenger_id AS passenger_bk, -- Исправлено: в dim_passengers BK называется passenger_id
|
|
p.passenger_name,
|
|
(bm.last_flight_date - bm.first_flight_date) AS days_as_customer
|
|
FROM base_metrics bm
|
|
JOIN dds.dim_passengers p ON bm.passenger_sk = p.passenger_sk
|
|
JOIN fare_modes fm ON bm.passenger_sk = fm.passenger_sk;
|
|
|
|
-- Шаг 3: UPDATE существующих записей.
|
|
UPDATE dm.passenger_loyalty AS tgt
|
|
SET
|
|
passenger_name = src.passenger_name,
|
|
total_bookings = src.total_bookings,
|
|
total_flights = src.total_flights,
|
|
total_boarded = src.total_boarded,
|
|
total_spent = src.total_spent,
|
|
avg_ticket_price = (src.total_spent / NULLIF(src.total_flights, 0))::NUMERIC(10,2),
|
|
favorite_fare_conditions = src.favorite_fare_conditions,
|
|
unique_routes = src.unique_routes,
|
|
first_flight_date = src.first_flight_date,
|
|
last_flight_date = src.last_flight_date,
|
|
days_as_customer = src.days_as_customer,
|
|
updated_at = now(),
|
|
_load_id = '{{ run_id }}',
|
|
_load_ts = now()
|
|
FROM tmp_passenger_delta AS src
|
|
WHERE tgt.passenger_sk = src.passenger_sk
|
|
AND (
|
|
-- Обновляем только если что-то реально изменилось
|
|
tgt.total_flights IS DISTINCT FROM src.total_flights
|
|
OR tgt.total_boarded IS DISTINCT FROM src.total_boarded
|
|
OR tgt.total_spent IS DISTINCT FROM src.total_spent
|
|
OR tgt.last_flight_date IS DISTINCT FROM src.last_flight_date
|
|
OR tgt.passenger_name IS DISTINCT FROM src.passenger_name
|
|
);
|
|
|
|
-- Шаг 4: INSERT новых пассажиров.
|
|
INSERT INTO dm.passenger_loyalty (
|
|
passenger_sk,
|
|
passenger_bk,
|
|
passenger_name,
|
|
total_bookings,
|
|
total_flights,
|
|
total_boarded,
|
|
total_spent,
|
|
avg_ticket_price,
|
|
favorite_fare_conditions,
|
|
unique_routes,
|
|
first_flight_date,
|
|
last_flight_date,
|
|
days_as_customer,
|
|
_load_id
|
|
)
|
|
SELECT
|
|
src.passenger_sk,
|
|
src.passenger_bk,
|
|
src.passenger_name,
|
|
src.total_bookings,
|
|
src.total_flights,
|
|
src.total_boarded,
|
|
src.total_spent,
|
|
(src.total_spent / NULLIF(src.total_flights, 0))::NUMERIC(10,2),
|
|
src.favorite_fare_conditions,
|
|
src.unique_routes,
|
|
src.first_flight_date,
|
|
src.last_flight_date,
|
|
src.days_as_customer,
|
|
'{{ run_id }}' AS _load_id
|
|
FROM tmp_passenger_delta AS src
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM dm.passenger_loyalty AS tgt
|
|
WHERE tgt.passenger_sk = src.passenger_sk
|
|
);
|