Files
airflow-greenplum/sql/dm/passenger_loyalty_load.sql
T
ddadminandClaude Sonnet 4.6 815a248841 chore(bookings): полировка эталонного среза — код, документация, ANALYZE
- Зачем:
  - подготовка эталонного пайплайна к курсовой: код должен быть чистым,
    документация — актуальной, все слои — консистентными.
- Что:
  - добавлен ANALYZE во все 5 DM load-скриптов (был в STG/ODS/DDS, пропущен в DM).
  - README актуализирован: добавлен DM-слой в быстрый старт, список DAG'ов,
    исправлен дефолт BOOKINGS_JOBS (2→1), добавлены примеры запросов DM.
  - удалены остатки CSV-пайплайна: CSV_DIR/CSV_ROWS из docs/stack.md.
  - docs/stack.md: исправлен дефолт BOOKINGS_JOBS (2→1).
  - убран отладочный комментарий «Исправлено:» в passenger_loyalty_load.sql.
  - исправлено форматирование (black) в 4 файлах.
  - TODO.md: отмечены пройденные пункты Этапа 1.5.
- Проверка:
  - make lint && make test — проходят.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 19:37:35 +03:00

132 lines
5.8 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,
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
);
ANALYZE dm.passenger_loyalty;