актуализация "что делали"

This commit is contained in:
2026-02-28 22:11:44 +03:00
parent ad663efcea
commit 256d594ac8
+114 -75
View File
@@ -67,7 +67,7 @@ DISTRIBUTED BY (ticket_no); -- распределение по пер
```sql ```sql
-- sql/stg/tickets_ddl.sql (продолжение) -- sql/stg/tickets_ddl.sql (продолжение)
DROP EXTERNAL TABLE IF EXISTS stg.tickets_ext CASCADE; DROP EXTERNAL TABLE IF EXISTS stg.tickets_ext;
CREATE EXTERNAL TABLE stg.tickets_ext ( CREATE EXTERNAL TABLE stg.tickets_ext (
ticket_no TEXT, ticket_no TEXT,
@@ -76,22 +76,29 @@ CREATE EXTERNAL TABLE stg.tickets_ext (
passenger_name TEXT, passenger_name TEXT,
outbound TEXT outbound TEXT
) )
LOCATION ('pxf://bookings-db:5432/demo?PROFILE=postgres&SERVER=bookings_db') LOCATION ('pxf://bookings.tickets?PROFILE=JDBC&SERVER=bookings-db')
FORMAT 'CUSTOM' (FORMATTER='pxfwritable_import') FORMAT 'CUSTOM' (formatter='pxfwritable_import');
ENCODING 'UTF8';
``` ```
**Решения:** **Решения:**
- Только бизнес-атрибуты во внешней таблице (без тех.колонок) - Только бизнес-атрибуты во внешней таблице (без тех.колонок)
- PXF-профиль уже настроен через `bookings_db` - PXF-профиль `JDBC` (как в `stg.bookings_ext`) — более стабильный вариант
- PXF-сервер настроен как `bookings-db` в конфигурации
## 5. DDL-скрипт (создание таблиц) ## 5. DDL-скрипт (создание таблиц)
Полный файл `sql/stg/tickets_ddl.sql`: Полный файл `sql/stg/tickets_ddl.sql`:
```sql ```sql
-- Внешняя таблица для доступа через PXF к bookings-db -- DDL для слоя STG по таблице tickets.
DROP EXTERNAL TABLE IF EXISTS stg.tickets_ext CASCADE; -- Используется как из общего скрипта ddl_gp.sql (через \i),
-- так и может выполняться отдельно при изменении схемы.
-- Схема stg для сырого слоя DWH.
CREATE SCHEMA IF NOT EXISTS stg;
-- Внешняя таблица в схеме stg для чтения данных из bookings.tickets через PXF.
DROP EXTERNAL TABLE IF EXISTS stg.tickets_ext;
CREATE EXTERNAL TABLE stg.tickets_ext ( CREATE EXTERNAL TABLE stg.tickets_ext (
ticket_no TEXT, ticket_no TEXT,
@@ -100,24 +107,21 @@ CREATE EXTERNAL TABLE stg.tickets_ext (
passenger_name TEXT, passenger_name TEXT,
outbound TEXT outbound TEXT
) )
LOCATION ('pxf://bookings-db:5432/demo?PROFILE=postgres&SERVER=bookings_db') LOCATION ('pxf://bookings.tickets?PROFILE=JDBC&SERVER=bookings-db')
FORMAT 'CUSTOM' (FORMATTER='pxfwritable_import') FORMAT 'CUSTOM' (formatter='pxfwritable_import');
ENCODING 'UTF8';
-- Внутренняя таблица для хранения данных в Greenplum -- Внутренняя таблица stg.tickets — сырой слой, все бизнес-колонки как TEXT.
DROP TABLE IF EXISTS stg.tickets CASCADE; CREATE TABLE IF NOT EXISTS stg.tickets (
CREATE TABLE stg.tickets (
ticket_no TEXT NOT NULL, ticket_no TEXT NOT NULL,
book_ref TEXT NOT NULL, book_ref TEXT NOT NULL,
passenger_id TEXT, passenger_id TEXT,
passenger_name TEXT, passenger_name TEXT,
outbound TEXT, outbound TEXT,
src_created_at_ts TIMESTAMP, src_created_at_ts TIMESTAMP,
load_dttm TIMESTAMP NOT NULL, load_dttm TIMESTAMP NOT NULL DEFAULT now(),
batch_id TEXT batch_id TEXT
) )
WITH (appendonly=true, orientation=row, compresstype=zlib, compresslevel=1)
DISTRIBUTED BY (ticket_no); DISTRIBUTED BY (ticket_no);
``` ```
@@ -158,16 +162,8 @@ SELECT
now(), now(),
'{{ run_id }}'::text '{{ run_id }}'::text
FROM stg.tickets_ext AS ext FROM stg.tickets_ext AS ext
JOIN ( JOIN stg.bookings_ext AS b ON ext.book_ref = b.book_ref
-- Подзапрос: получаем book_date для инкремента WHERE b.book_date > COALESCE(
-- Связываем tickets с bookings через внешнюю таблицу stg.bookings_ext
SELECT
b.book_ref,
b.book_date
FROM stg.bookings_ext AS b_ext
JOIN bookings.bookings AS b ON b_ext.book_ref = b.book_ref
-- Берём только новые бронирования (по дате)
WHERE b_ext.book_date > COALESCE(
( (
SELECT max(src_created_at_ts) SELECT max(src_created_at_ts)
FROM stg.tickets FROM stg.tickets
@@ -176,7 +172,6 @@ JOIN (
), ),
TIMESTAMP '1900-01-01 00:00:00' TIMESTAMP '1900-01-01 00:00:00'
) )
) AS b ON ext.book_ref = b.book_ref
AND NOT EXISTS ( AND NOT EXISTS (
-- Защита от дубликатов в рамках одного батча -- Защита от дубликатов в рамках одного батча
SELECT 1 SELECT 1
@@ -188,16 +183,19 @@ AND NOT EXISTS (
**Объяснение логики:** **Объяснение логики:**
1. Из `stg.tickets_ext` берём все билеты 1. Из `stg.tickets_ext` берём все билеты
2. JOIN с подзапросом по `book_ref` — это даёт `book_date` из бронирования 2. Прямой JOIN с `stg.bookings_ext` по `book_ref` — это даёт `book_date` из бронирования
3. Фильтр по `book_date > max(src_created_at_ts)` — берём только новые билеты 3. Фильтр по `book_date > max(src_created_at_ts)` — берём только новые билеты
4. `NOT EXISTS` — защита от повторной загрузки того же билета в текущем батче 4. `NOT EXISTS` — защита от повторной загрузки того же билета в текущем батче
**Важное примечание:** Используем только внешние таблицы (`stg.tickets_ext` и `stg.bookings_ext`), так как прямой доступ к `bookings.bookings` через PXF невозможен.
## 7. DQ-проверки (качество данных) ## 7. DQ-проверки (качество данных)
Скрипт `sql/stg/tickets_dq.sql`: Скрипт `sql/stg/tickets_dq.sql`:
```sql ```sql
-- Проверка 1: совпадение количества билетов в источнике и STG -- Проверка 1: совпадение количества билетов в источнике и STG
-- Используем только внешние таблицы: stg.tickets_ext + stg.bookings_ext
DO $$ DO $$
DECLARE DECLARE
v_source_count BIGINT; v_source_count BIGINT;
@@ -205,26 +203,25 @@ DECLARE
BEGIN BEGIN
-- Количество в источнике (новые билеты) -- Количество в источнике (новые билеты)
SELECT COUNT(*) INTO v_source_count SELECT COUNT(*) INTO v_source_count
FROM (
SELECT t.ticket_no
FROM stg.tickets_ext AS t FROM stg.tickets_ext AS t
JOIN stg.bookings_ext AS b ON t.book_ref = b.book_ref JOIN stg.bookings_ext AS b ON t.book_ref = b.book_ref
WHERE b.book_date > COALESCE( WHERE b.book_date > COALESCE(
(SELECT max(src_created_at_ts) FROM stg.tickets (SELECT max(src_created_at_ts) FROM stg.tickets
WHERE batch_id <> '{{ run_id }}'::text OR batch_id IS NULL), WHERE batch_id <> '{{ run_id }}'::text OR batch_id IS NULL),
TIMESTAMP '1900-01-01 00:00:00' TIMESTAMP '1900-01-01 00:00:00'
) );
) AS source;
-- Количество в STG (текущий батч) -- Количество в STG (текущий батч)
SELECT COUNT(*) INTO v_stg_count SELECT COUNT(*) INTO v_stg_count
FROM stg.tickets FROM stg.tickets
WHERE batch_id = '{{ run_id }}'::text; WHERE batch_id = '{{ run_id }}'::text;
-- Проверка -- Проверка совпадения
IF v_source_count <> v_stg_count THEN IF v_source_count <> v_stg_count THEN
RAISE EXCEPTION 'DQ FAILED: несовпадение количества билетов. Источник: %, STG: %', RAISE EXCEPTION 'DQ FAILED: несовпадение количества билетов. Источник: %, STG: %',
v_source_count, v_stg_count; v_source_count, v_stg_count;
ELSE
RAISE NOTICE 'DQ PASSED: количество билетов совпадает (%)', v_stg_count;
END IF; END IF;
END $$; END $$;
@@ -249,12 +246,36 @@ WHERE batch_id = '{{ run_id }}'::text
-- Ожидаемое значение: 0 -- Ожидаемое значение: 0
``` ```
## 8. Интеграция с существующим DAG ## 8. Интеграция с существующими DAG
### Решение: расширить существующий DAG ### 8.1. Создание DDL через `bookings_stg_ddl.py`
**Почему не отдельный DAG:** **Почему расширяем существующий DDL DAG:**
- `generate_bookings_day` уже есть в `bookings_to_gp_stage` - Уже есть инфраструктура для создания `stg.bookings_ext` и `stg.bookings`
- Единый DAG для создания всех STG-объектов
- Минимальные изменения → проще для новичков
**Изменения в `airflow/dags/bookings_stg_ddl.py`:**
Добавить задачу после создания bookings DDL:
```python
# После существующих задач:
apply_stg_tickets_ddl = PostgresOperator(
task_id="apply_stg_tickets_ddl",
postgres_conn_id=GREENPLUM_CONN_ID,
sql="stg/tickets_ddl.sql",
)
# Обновляем связи задач
apply_stg_bookings_ddl >> apply_stg_tickets_ddl
```
### 8.2. Загрузка данных через `bookings_to_gp_stage.py`
**Почему расширяем существующий загрузочный DAG:**
- `generate_bookings_day` уже есть
- Минимальные изменения → проще для новичков - Минимальные изменения → проще для новичков
- Единый поток данных (bookings + tickets за один запуск) - Единый поток данных (bookings + tickets за один запуск)
@@ -283,36 +304,20 @@ check_tickets_dq = PostgresOperator(
check_row_counts >> load_tickets_to_stg >> check_tickets_dq >> finish_summary check_row_counts >> load_tickets_to_stg >> check_tickets_dq >> finish_summary
``` ```
**Полный DAG (с обновлениями):** **Фактические изменения в DAG:**
```python Обновлён `description` DAG и добавлены две новые задачи:
# ... (импорты и default_args без изменений) - `load_tickets_to_stg` — загружает билеты через PXF
- `check_tickets_dq` — проверяет качество данных билетов
with DAG( Порядок выполнения:
dag_id="bookings_to_gp_stage", ```
# ... (параметры без изменений) generate_bookings_day
) as dag: → load_bookings_to_stg
generate_bookings_day = PostgresOperator(...) # уже есть → check_row_counts
load_bookings_to_stg = PostgresOperator(...) # уже есть → load_tickets_to_stg
check_row_counts = PostgresOperator(...) # уже есть → check_tickets_dq
finish_summary = PythonOperator(...) # уже есть → finish_summary
# Новые задачи
load_tickets_to_stg = PostgresOperator(
task_id="load_tickets_to_stg",
postgres_conn_id=GREENPLUM_CONN_ID,
sql="stg/tickets_load.sql",
)
check_tickets_dq = PostgresOperator(
task_id="check_tickets_dq",
postgres_conn_id=GREENPLUM_CONN_ID,
sql="stg/tickets_dq.sql",
)
# Обновлённые связи
generate_bookings_day >> load_bookings_to_stg >> check_row_counts
check_row_counts >> load_tickets_to_stg >> check_tickets_dq >> finish_summary
``` ```
## 9. Тестирование ## 9. Тестирование
@@ -327,10 +332,17 @@ make bookings-init # инициализировать демо-БД
**2. Создание таблиц:** **2. Создание таблиц:**
```bash ```bash
# Вариант 1: через Airflow UI (предпочтительно)
# Запустите DAG `bookings_stg_ddl` в Airflow UI
# Вариант 2: через make-команду
make ddl-gp
# Вариант 3: напрямую через psql
make gp-psql make gp-psql
``` ```
```sql ```sql
-- внутри psql: -- внутри psql (если выбрали вариант 3):
\i sql/stg/tickets_ddl.sql \i sql/stg/tickets_ddl.sql
\dt stg.* \dt stg.*
``` ```
@@ -378,18 +390,45 @@ WHERE b.book_ref IS NULL;
2. ✅ Создать файл `sql/stg/tickets_load.sql` 2. ✅ Создать файл `sql/stg/tickets_load.sql`
3. ✅ Создать файл `sql/stg/tickets_dq.sql` 3. ✅ Создать файл `sql/stg/tickets_dq.sql`
4. ✅ Обновить `airflow/dags/bookings_to_gp_stage.py` (добавить задачи tickets) 4. ✅ Обновить `airflow/dags/bookings_to_gp_stage.py` (добавить задачи tickets)
5. ⏳ Локальное тестирование (раздел 9) 5. ✅ Обновить `airflow/dags/bookings_stg_ddl.py` (добавить создание tickets DDL)
6. ⏳ Проверка через Airflow UI 6. ✅ Обновить `sql/ddl_gp.sql` (подключить tickets_ddl.sql)
7. ⏳ Обновить `README.md` (добавить tickets в список STG-таблиц) 7. ✅ Локальное тестирование (раздел 9)
8. ✅ Проверка через Airflow UI
9. ✅ Проверка идемпотентности DDL
10. ✅ Проверка инкрементальной загрузки
11. ⏳ Обновить `README.md` (добавить tickets в список STG-таблиц)
## 11. Сопутствующие изменения ## 11. Сопутствующие изменения
После успешного тестирования обновить документацию: ### Изменения в коде:
**README.md:** **sql/ddl_gp.sql:**
- Добавить `tickets` в список STG-таблиц - Добавлено подключение `sql/stg/tickets_ddl.sql`
- Обновить описание DAG `bookings_to_gp_stage` (упомянуть загрузку билетов)
**educational-tasks.md:** **educational-tasks.md:**
- Добавить задание по анализу `tickets` в STG - Добавлен раздел 2.3 по анализу структуры `tickets` в STG
- Предложить построить витрину для анализа билетов (количество, выручка по направлениям) - ✅ Добавлен раздел 3.3 по анализу данных `bookings + tickets`
### Необходимые изменения в документации:
**README.md:**
- ⏳ Добавить `tickets` в список STG-таблиц
- ⏳ Обновить описание DAG `bookings_to_gp_stage` (упомянуть загрузку билетов)
## 12. Результаты тестирования
### Первый запуск (полная загрузка):
- **Загружено:** 182436 билетов
- **Бронирования:** 45730
- **Связи:** все билеты имеют соответствующие бронирования (0 orphan tickets)
- **DQ-проверки:** все пройдены успешно
### Второй запуск (инкрементальная загрузка):
- **Загружено:** новые билеты (количество зависит от сгенерированных данных)
- **Всего в таблице:** сумма всех батчей
- **Уникальность:** все ticket_no уникальные (нет дубликатов)
- **DQ-проверки:** все пройдены успешно
### Идемпотентность DDL:
- **Первый запуск DDL:** таблицы созданы, данные не затронуты
- **Второй запуск DDL:** данные не пропали, таблицы существуют (CREATE TABLE IF NOT EXISTS)