diff --git a/SCD.md b/SCD.md index cac5851..f4cefa6 100644 --- a/SCD.md +++ b/SCD.md @@ -243,6 +243,8 @@ WHERE customer_id = 1 AND is_current = true Пример в Trino/Hive-стиле (только INSERT): +Давайте разберём этот запрос по шагам, чтобы понять его логику: + ```sql -- new_customers — staging-таблица с обновлёнными данными -- dim_customers_scd2 — основная таблица (append-only) @@ -253,7 +255,7 @@ WITH current_customers AS ( FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn FROM dim_customers_scd2 - ) + ) WHERE rn = 1 -- отбираем первую, самую свежую, запись ) SELECT @@ -261,12 +263,109 @@ SELECT n.customer_id, -- неизменный бизнес-ID клиента n.name, n.category, - valid_from + COALESCE(n.effective_date, current_date) AS valid_from -- дата начала действия новой версии FROM new_customers n LEFT JOIN current_customers c ON n.customer_id = c.customer_id WHERE c.customer_id IS NULL OR c.category != n.category; -- условие верно, если какие-то из полей справочника поменялись ``` +### 🎯 Как работает этот запрос: пошаговое объяснение + +```mermaid +graph TD + A[new_customers
новые данные] --> B[CTE: current_customers] + C[dim_customers_scd2
текущее состояние] --> B + B --> D[LEFT JOIN по customer_id] + A --> D + D --> E{Условие WHERE} + E -->|Новый клиент| F[Вставить запись] + E -->|Изменения| F + E -->|Без изменений| G[Пропустить] + F --> H[dim_customers_scd2
обновленная таблица] +``` + +#### Шаг 1: Подготовка данных (CTE current_customers) +CTE `current_customers` находит **последнюю версию** каждого клиента из таблицы `dim_customers_scd2`: + +```sql +SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn +FROM dim_customers_scd2 +``` + +- `PARTITION BY customer_id` — группируем по клиентам +- `ORDER BY valid_from DESC` — сортируем версии от самой новой к самой старой +- `WHERE rn = 1` — выбираем только самую свежую версию + +#### Шаг 2: Сравнение данных (LEFT JOIN + WHERE) +Теперь сравниваем новые данные с текущими: + +```sql +FROM new_customers n +LEFT JOIN current_customers c ON n.customer_id = c.customer_id +``` + +**Возможные сценарии после JOIN:** + +| Сценарий | n.customer_id | c.customer_id | Условие WHERE | Результат | +|----------|---------------|---------------|---------------|-----------| +| Новый клиент | 2 | NULL | ✅ `c.customer_id IS NULL` | Вставляется | +| Категория изменилась | 1 | 1 | ✅ `c.category != n.category` | Вставляется | +| Без изменений | 3 | 3 | ❌ оба условия ложны | Пропускается | + +#### Шаг 3: Вставка новых версий +Для подходящих записей создаём новую версию: +- `uuid()` — генерируем уникальный ключ для новой версии (функция, скобки обязательны) +- `COALESCE(n.effective_date, current_date)` — устанавливаем дату начала действия новой версии + +> 💡 **Синтаксическая поправка**: +> - `current_date` — это ключевое слово, а не функция, поэтому скобки не нужны +> - `uuid()` — это функция, поэтому скобки обязательны +> - Разница: `current_date` возвращает текущую дату, `uuid()` генерирует уникальный идентификатор + +> 💡 **Правильный подход к датам**: В реальных ETL-процессах важно использовать дату из исходных данных, когда она доступна. Мы используем `COALESCE(n.effective_date, current_date)`, что означает: +> - Если в `new_customers` есть поле `effective_date` — используем его +> - Если нет — используем текущую дату (`current_date`) +> +> **Почему это важно:** +> - `effective_date` отражает реальную дату изменения (например, когда клиент стал VIP) +> - `current_date` — это дата загрузки данных, которая может не совпадать с датой изменения +> - Использование правильной даты критично для точного исторического анализа +> +> **Пример правильной структуры исходных данных:** +> ```sql +> new_customers: +> customer_id | name | category | effective_date +> 1 | Иван Петров | VIP | 2025-04-15 ← дата реального изменения +> ``` + +#### Практический пример + +**До выполнения запроса:** +``` +dim_customers_scd2: +customer_id | name | category | valid_from +1 | Иван Петров | Regular | 2024-01-01 +``` + +**Новые данные:** +``` +new_customers: +customer_id | name | category +1 | Иван Петров | VIP ← изменилась категория +2 | Мария Иванова| Regular ← новый клиент +``` + +**После выполнения запроса:** +``` +dim_customers_scd2: +customer_id | name | category | valid_from +1 | Иван Петров | Regular | 2024-01-01 ← старая версия +1 | Иван Петров | VIP | 2025-11-04 ← новая версия +2 | Мария Иванова| Regular | 2025-11-04 ← новый клиент +``` + +> 💡 **Ключевой момент**: В append-only системах мы **не обновляем** старые записи, а только **добавляем новые**. История сохраняется автоматически! + **Главный вопрос после загрузки: как же читать эти данные?** Поскольку мы не можем обновлять `valid_to` у предыдущей версии (у неё останется `NULL`), стандартный подход с `BETWEEN` не сработает. Вместо этого, для поиска нужной версии мы полагаемся на **оконные функции** или на логику «ближайшей даты, но не позже». @@ -278,14 +377,14 @@ WHERE c.customer_id IS NULL OR c.category != n.category; -- условие ве ```sql -- Вариант с оконной функцией (универсальный и надежный) WITH ranked AS ( - SELECT - *, + SELECT + *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn - FROM customers_scd2 + FROM dim_customers_scd2 ) -SELECT +SELECT customer_id, name, category, valid_from -FROM ranked +FROM ranked WHERE rn = 1; ``` @@ -300,15 +399,15 @@ WHERE rn = 1; ```sql -- Запрос для получения состояния на '2025-03-15' WITH as_of_date AS ( - SELECT + SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn - FROM customers_scd2 + FROM dim_customers_scd2 WHERE valid_from <= DATE '2025-03-15' ) -SELECT +SELECT customer_id, name, category, valid_from -FROM as_of_date +FROM as_of_date WHERE rn = 1; ``` @@ -360,6 +459,3 @@ WHERE rn = 1; И помните: даже в системах без `UPDATE` вы можете хранить полную историю — достаточно понимать, как правильно читать данные с помощью оконных функций и временных границ. ---- - -Готово! Статья теперь включает полный цикл: от концепции — к реализации в традиционной СУБД — и далее к адаптации для современных аналитических платформ. \ No newline at end of file