- fact_flight_sales_dq: student SK (passenger/route/airplane) → NOTICE, airport_sk → порог 1% (эталон через ods.routes) - routes_dq: закомментирован RI airplane_code → ods.airplanes - ODS DAG: убрана зависимость dq_ods_airplanes → load_ods_routes - 18 студенческих файлов заменены заглушками (ODS/DDS/DM load+dq) - test_ods_sql_contract: SNAPSHOT_ENTITIES без airplanes/seats - test_dags_smoke: убран assert dq_ods_airplanes → dq_ods_routes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
170 lines
5.8 KiB
SQL
170 lines
5.8 KiB
SQL
-- DQ для DDS fact_flight_sales.
|
|
|
|
DO $$
|
|
DECLARE
|
|
v_row_count BIGINT;
|
|
v_ods_count BIGINT;
|
|
v_dup_count BIGINT;
|
|
v_null_passenger BIGINT;
|
|
v_null_tariff BIGINT;
|
|
v_null_airport BIGINT;
|
|
v_null_student BIGINT;
|
|
v_null_calendar BIGINT;
|
|
v_null_required BIGINT;
|
|
BEGIN
|
|
-- Для пустого инкрементального окна (ods.segments) допускаем пустой факт.
|
|
SELECT COUNT(*)
|
|
INTO v_ods_count
|
|
FROM ods.segments;
|
|
|
|
SELECT COUNT(*)
|
|
INTO v_row_count
|
|
FROM dds.fact_flight_sales;
|
|
|
|
IF v_ods_count = 0 THEN
|
|
IF v_row_count <> 0 THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: ods.segments пустая, но в dds.fact_flight_sales есть строки: %',
|
|
v_row_count;
|
|
END IF;
|
|
|
|
RAISE NOTICE
|
|
'DQ PASSED: ods.segments и dds.fact_flight_sales пустые (инкрементальное окно без сегментов).';
|
|
RETURN;
|
|
END IF;
|
|
|
|
IF v_row_count = 0 THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: dds.fact_flight_sales пуста при непустом источнике ods.segments (%).',
|
|
v_ods_count;
|
|
END IF;
|
|
|
|
-- Покрытие: количество строк = ods.segments.
|
|
IF v_row_count <> v_ods_count THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: dds.fact_flight_sales (%) <> ods.segments (%). Потеряны строки.',
|
|
v_row_count,
|
|
v_ods_count;
|
|
END IF;
|
|
|
|
-- Нет дублей по зерну.
|
|
SELECT COUNT(*)
|
|
INTO v_dup_count
|
|
FROM (
|
|
SELECT ticket_no, flight_id
|
|
FROM dds.fact_flight_sales
|
|
GROUP BY ticket_no, flight_id
|
|
HAVING COUNT(*) > 1
|
|
) AS d;
|
|
|
|
IF v_dup_count <> 0 THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: в dds.fact_flight_sales дубликаты (ticket_no, flight_id): %',
|
|
v_dup_count;
|
|
END IF;
|
|
|
|
-- Учебный комментарий: passenger_sk будет NULL, пока вы не реализуете dim_passengers.
|
|
-- После реализации: TRUNCATE dds.fact_flight_sales → перезагрузка → все SK заполнены.
|
|
-- Полную версию DQ (с блокировкой) см. в ветке solution.
|
|
SELECT COUNT(*)
|
|
INTO v_null_passenger
|
|
FROM dds.fact_flight_sales
|
|
WHERE passenger_sk IS NULL;
|
|
|
|
IF v_null_passenger <> 0 THEN
|
|
RAISE NOTICE
|
|
'DQ INFO: студенческий SK (passenger) NULL: %. После реализации dim_passengers: TRUNCATE fact → перезагрузка.',
|
|
v_null_passenger;
|
|
END IF;
|
|
|
|
-- Ссылочная целостность: tariff_sk.
|
|
SELECT COUNT(*)
|
|
INTO v_null_tariff
|
|
FROM dds.fact_flight_sales
|
|
WHERE tariff_sk IS NULL;
|
|
|
|
IF v_null_tariff <> 0 THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: в fact_flight_sales строки без tariff_sk: %',
|
|
v_null_tariff;
|
|
END IF;
|
|
|
|
-- departure_airport_sk и arrival_airport_sk заполняются через ods.routes (эталон).
|
|
-- NULL здесь — аномалия данных (пропущен маршрут в ODS), а не отсутствие студенческого кода.
|
|
-- Порог 1% — защита от единичных аномалий источника (аналогично calendar_sk).
|
|
SELECT COUNT(*)
|
|
INTO v_null_airport
|
|
FROM dds.fact_flight_sales
|
|
WHERE departure_airport_sk IS NULL
|
|
OR arrival_airport_sk IS NULL;
|
|
|
|
IF v_null_airport > 0 THEN
|
|
IF v_null_airport * 100.0 / NULLIF(v_row_count, 0) > 1.0 THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: NULL airport_sk: % (>1%%)',
|
|
v_null_airport;
|
|
ELSE
|
|
RAISE NOTICE
|
|
'DQ WARNING: NULL airport_sk: % (<=1%%, допустимо)',
|
|
v_null_airport;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- route_sk и airplane_sk будут NULL, пока вы не реализуете dim_routes.
|
|
-- Не блокируем pipeline.
|
|
-- Полную версию DQ (с блокировкой) см. в ветке solution.
|
|
SELECT COUNT(*)
|
|
INTO v_null_student
|
|
FROM dds.fact_flight_sales
|
|
WHERE route_sk IS NULL
|
|
OR airplane_sk IS NULL;
|
|
|
|
IF v_null_student > 0 THEN
|
|
RAISE NOTICE
|
|
'DQ INFO: студенческие SK (route/airplane) NULL: %. После реализации dim_routes: TRUNCATE fact → перезагрузка.',
|
|
v_null_student;
|
|
END IF;
|
|
|
|
-- Calendar: допустимо если scheduled_departure IS NULL, фейлим если > 1%.
|
|
SELECT COUNT(*)
|
|
INTO v_null_calendar
|
|
FROM dds.fact_flight_sales
|
|
WHERE calendar_sk IS NULL;
|
|
|
|
IF v_null_calendar > 0 THEN
|
|
IF v_null_calendar * 100.0 / NULLIF(v_row_count, 0) > 1.0 THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: в fact_flight_sales слишком много строк без calendar_sk: % (>1%%)',
|
|
v_null_calendar;
|
|
ELSE
|
|
RAISE NOTICE
|
|
'DQ WARNING: в fact_flight_sales строк без calendar_sk: % (<=1%%, допустимо)',
|
|
v_null_calendar;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Обязательные поля.
|
|
SELECT COUNT(*)
|
|
INTO v_null_required
|
|
FROM dds.fact_flight_sales
|
|
WHERE book_ref IS NULL
|
|
OR book_ref = ''
|
|
OR ticket_no IS NULL
|
|
OR ticket_no = ''
|
|
OR flight_id IS NULL
|
|
OR is_boarded IS NULL
|
|
OR _load_id IS NULL
|
|
OR _load_id = ''
|
|
OR _load_ts IS NULL;
|
|
|
|
IF v_null_required <> 0 THEN
|
|
RAISE EXCEPTION
|
|
'DQ FAILED: в fact_flight_sales NULL обязательные поля: %',
|
|
v_null_required;
|
|
END IF;
|
|
|
|
RAISE NOTICE
|
|
'DQ PASSED: dds.fact_flight_sales ок, строк=%',
|
|
v_row_count;
|
|
END $$;
|