Перейти к основному содержимому

Версии моделей

Версии моделей, версии dbt_project.yml и версии .yml

Обратите внимание, что версии моделей отличаются от версий dbt_project.yml и версий файлов свойств .yml.

Версии моделей — это функция, которая позволяет улучшить управление и контроль моделей данных, позволяя отслеживать изменения и обновления моделей с течением времени. Версии dbt_project.yml относятся к совместимости проекта dbt с определенной версией dbt. Номера версий в файлах свойств .yml указывают, как dbt интерпретирует эти YAML файлы. Последние два являются полностью необязательными, начиная с dbt версии 1.5.

Версионирование API — сложная задача в программной инженерии. Корень проблемы в том, что у производителей и потребителей API разные стимулы:

  • Производителям API необходимо иметь возможность изменять его логику и структуру. Существует реальная стоимость поддержания устаревших конечных точек навсегда, но потеря доверия пользователей гораздо дороже.
  • Потребители API должны доверять его стабильности: их запросы будут продолжать работать и не сломаются без предупреждения. Хотя миграция на более новую версию API требует затрат, незапланированная миграция обходится гораздо дороже.

Когда вы делитесь финальной моделью dbt с другими командами или системами, эта модель работает как API. Когда производителю этой модели необходимо внести значительные изменения, как они могут избежать нарушения запросов ее пользователей?

Версионирование моделей — это инструмент для решения этой проблемы, вдумчиво и напрямую. Цель не в том, чтобы полностью устранить проблему, и не в том, чтобы притвориться, что она проще, чем есть на самом деле.

Связанная документация

Зачем версионировать модель?

Если модель определяет "контракт" (набор гарантий для ее структуры), также возможно изменить структуру этой модели таким образом, что это нарушит предыдущий набор гарантий. Это может быть так же очевидно, как удаление или переименование столбца, или более тонко, как изменение его типа данных или допустимости null.

Один из подходов — заставить каждого потребителя модели немедленно справляться с нарушением изменений, как только они будут развернуты в производстве. Это на самом деле подходящий ответ для многих небольших организаций или при быстром итеративном развитии еще не зрелого набора моделей данных. Но это плохо масштабируется за пределами этого.

Вместо этого, для зрелых моделей в крупных организациях, которые поддерживают запросы внутри и вне dbt, владелец модели может использовать версии моделей, чтобы:

  • Тестировать "предрелизные" изменения (в производстве, в системах downstream)
  • Повышать последнюю версию, чтобы использовать ее как канонический источник истины
  • Предоставлять окно миграции с "старой" версии

В течение этого окна миграции, везде, где модель используется downstream, она может продолжать ссылаться на конкретную версию.

dbt Core 1.6 представил поддержку устаревания моделей путем указания deprecation_date. В совокупности, версии моделей и устаревание предлагают путь для производителей моделей закрыть старые модели, а потребителям — время для миграции через изменения, нарушающие совместимость. Это способ управления изменениями в организации: разработать новую версию, повысить последнюю, запланировать старую версию для устаревания, обновить downstream ссылки, а затем удалить старую версию.

Здесь существует реальный компромисс — стоимость частой миграции downstream кода и стоимость (и беспорядок) материализации нескольких версий модели в хранилище данных. Версии моделей не устраняют эту проблему, но, устанавливая дату устаревания и сообщая четкое окно для потребителей, чтобы они могли плавно мигрировать со старых версий, они устанавливают известные границы на стоимость этой миграции.

Когда следует версионировать модель?

Принуждая контракт модели, dbt может помочь вам поймать непреднамеренные изменения в именах столбцов и типах данных, которые могут вызвать большую головную боль для downstream запросов. Если вы вносите эти изменения намеренно, вам следует создать новую версию модели. Если вы вносите изменения, не нарушающие совместимость, вам не нужна новая версия — например, добавление нового столбца или исправление ошибки в расчете существующего столбца.

Конечно, можно изменить определение модели другими способами — пересчитывая столбец так, чтобы не изменять его имя, тип данных или enforceable характеристики, — но это существенно изменит результаты, видимые downstream запросами.

Это всегда вопрос суждения. Как поддерживающий широко используемую модель, вы лучше всех знаете, что является исправлением ошибки, а что — неожиданным изменением поведения.

Процесс закрытия и миграции версий моделей требует реальной работы и, вероятно, значительной координации между командами. Вы должны выбирать изменения, не нарушающие совместимость, когда это возможно. Однако неизбежно, что эти добавления, не нарушающие совместимость, оставят ваши самые важные модели с множеством неиспользуемых или устаревших столбцов.

Вместо того чтобы постоянно добавлять новую версию для каждого небольшого изменения, вы должны выбрать предсказуемый ритм (один или два раза в год, сообщаемый заранее), когда вы повышаете "последнюю" версию вашей модели, удаляя столбцы, которые больше не используются.

Чем это отличается от "контроля версий"?

Контроль версий позволяет вашей команде одновременно работать над одним репозиторием кода, управлять конфликтами между изменениями и просматривать изменения перед развертыванием в производстве. В этом смысле контроль версий является важным инструментом для версионирования развертывания всего проекта dbt — всегда в последнем состоянии ветки main. В общем, только одна версия вашего проектного кода развернута в среде в любой момент времени. Если что-то пойдет не так, у вас есть возможность откатить изменения, отменив коммит или pull request, или используя возможности платформы данных для "путешествия во времени".

Когда вы вносите обновления в исходный код модели — ее логическое определение на SQL или Python, или связанные конфигурации — dbt может сравнить ваш проект с предыдущим состоянием, позволяя вам перестраивать только те модели, которые изменились, и модели downstream от изменения. Таким образом, можно разрабатывать изменения в модели, быстро тестировать в CI и эффективно развертывать в производстве — все это координируется через вашу систему контроля версий.

Версионированные модели отличаются. Определение версий моделей уместно, когда люди, системы и процессы за пределами контроля вашей команды, внутри или вне dbt, зависят от ваших моделей. Вы не можете просто мигрировать их всех или нарушить их запросы по прихоти. Вам нужно предложить путь миграции с четкими различиями и датами устаревания.

Несколько версий модели будут существовать в одном репозитории кода одновременно и развертываться в одной и той же среде данных одновременно. Это похоже на то, как версии веб-API: несколько версий существуют одновременно, две или три, и не более). Со временем появляются новые версии, а старые версии закрываются.

Чем это отличается от простого создания новой модели?

Честно говоря, это отличается лишь немного! Здесь нет особой магии, и это сделано намеренно.

Вы всегда могли скопировать-вставить, создать новый файл модели и назвать его dim_customers_v2.sql. Почему вам следует выбрать "настоящую" версионированную модель вместо этого?

Как производитель версионированной модели:

  • Вы отслеживаете все активные версии в одном месте, а не разбросаны по всему коду
  • Вы можете повторно использовать конфигурацию модели и выделять только различия между версиями
  • Вы можете выбирать модели для сборки (или нет) в зависимости от того, являются ли они latest, prerelease или old версией
  • dbt уведомит потребителей вашей версионированной модели, когда новые версии станут доступны или когда они будут запланированы для устаревания

Как потребитель версионированной модели:

  • Вы используете согласованный ref, с возможностью закрепления на конкретной активной версии
  • Вы будете уведомлены на протяжении всего жизненного цикла версионированной модели

Все версии модели сохраняют оригинальное имя модели. Они ref'ятся по этому имени, а не по имени файла, в котором они определены. По умолчанию, ref разрешается на последнюю версию (как заявлено поддерживающим эту модель), но вы также можете ref конкретную версию модели с помощью ключевого слова version.

Предположим, что у dim_customers определены три версии: v2 является "последней", v3 — "предрелизной", а v1 — старой версией, которая все еще находится в окне устаревания. Поскольку v2 является последней версией, она получает особое отношение: она может быть определена в файле без суффикса, и ref('dim_customers') разрешится в v2, если версия не указана. Таблица ниже разбивает стандартные соглашения:

vверсияСинтаксис refИмя файлаОтношение в базе данных
3"предрелизная"ref('dim_customers', v=3)dim_customers_v3.sqlanalytics.dim_customers_v3
2"последняя"ref('dim_customers', v=2) и ref('dim_customers')dim_customers_v2.sql или dim_customers.sqlanalytics.dim_customers_v2 и analytics.dim_customers (рекомендуется)
1"старая"ref('dim_customers', v=1)dim_customers_v1.sqlanalytics.dim_customers_v1

Как вы увидите в разделе реализации ниже, версионированная модель может повторно использовать большинство своих YAML свойств и конфигураций. Каждая версия должна только указать, как она отличается от общего набора атрибутов. Это дает вам, как производителю версионированной модели, возможность выделить различия между версиями — что иначе трудно обнаружить в моделях с десятками или сотнями столбцов — и четко отслеживать, в одном месте, все версии модели, которые в настоящее время активны.

dbt также поддерживает выбор на основе version. Например, вы можете определить селектор YAML по умолчанию, который избегает запуска любых старых версий моделей в разработке, даже если вы продолжаете запускать их в производстве через период закрытия и миграции. (Вы могли бы достичь чего-то подобного, применяя tags к этим моделям и циклически меняя эти теги со временем.)

selectors.yml
selectors:
- name: exclude_old_versions
default: "{{ target.name == 'dev' }}"
definition:
method: fqn
value: "*"
exclude:
- method: version
value: old

Поскольку dbt знает, что эти модели на самом деле одна и та же модель, он может уведомлять downstream потребителей, когда новые версии становятся доступны, и когда старые версии запланированы для устаревания.

Найдено незакрепленное обращение к версионированной модели 'dim_customers'.
Разрешение на последнюю версию: my_model.v2
Доступна предрелизная версия 3. Она еще не была отмечена как 'последняя' ее поддерживающим.
Когда это произойдет, это обращение будет разрешаться в my_model.v3 вместо этого.

Попробуйте v3: {{ ref('my_dbt_project', 'my_model', v='3') }}
Закрепите на v2: {{ ref('my_dbt_project', 'my_model', v='2') }}

Как создать новую версию модели

Чаще всего вы начнете с модели, которая еще не имеет версий. Давайте вернемся во времени, когда dim_customers была простой автономной моделью с принудительным контрактом. Для простоты предположим, что у нее есть только два столбца, customer_id и country_name, хотя у большинства зрелых моделей будет гораздо больше.

models/dim_customers.sql
-- много sql

final as (

select
customer_id,
country_name
from ...

)

select * from final
models/schema.yml
models:
- name: dim_customers
config:
materialized: table
contract:
enforced: true
columns:
- name: customer_id
description: Это первичный ключ
data_type: int
- name: country_name
description: Где живет этот клиент
data_type: varchar

Предположим, вам нужно внести изменения, нарушающие совместимость, в модель: удалить столбец country_name, который больше не является надежным. Сначала создайте новый файл модели (SQL или Python), охватывающий эти изменения, нарушающие совместимость.

По умолчанию, соглашение заключается в том, чтобы называть новый файл с суффиксом _v<version>. Давайте создадим новый файл с именем dim_customers_v2.sql. (Нам не нужно переименовывать существующий файл модели пока, так как он все еще является "последней" версией.)

models/dim_customers_v2.sql
-- много sql

final as (

select
customer_id
-- country_name был удален!
from ...

)

select * from final

Теперь вы могли бы определить свойства и конфигурацию для dim_customers_v2 как новую автономную модель, без фактического отношения к dim_customers, кроме поразительного сходства. Вместо этого мы собираемся заявить, что это версии одной и той же модели, обе названные dim_customers. Мы можем определить их общие свойства, а затем только выделить различия между ними. (Или вы можете выбрать определение каждой версии модели с полными спецификациями и повторить значения, которые у них общие.)

models/schema.yml
models:
- name: dim_customers
latest_version: 1
config:
materialized: table
contract: {enforced: true}
columns:
- name: customer_id
description: Это первичный ключ
data_type: int
- name: country_name
description: Где живет этот клиент
data_type: varchar

# Объявите версии и выделите различия
versions:

- v: 1
# Соответствует тому, что выше -- ничего больше не нужно

- v: 2
# Удален столбец -- это изменение, нарушающее совместимость!
columns:
# Это означает: используйте список 'columns' из выше, но исключите country_name
- include: all
exclude: [country_name]

Конфигурация выше говорит: Вместо двух несвязанных моделей у меня есть два версионированных определения одной и той же модели: dim_customers_v1 и dim_customers_v2.

Где они определены? dbt ожидает, что каждая версия модели будет определена в файле с именем <model_name>_v<v>. В этом случае: dim_customers_v1.sql и dim_customers_v2.sql. Также возможно определить "последнюю" версию в dim_customers.sql (без суффикса), без дополнительной конфигурации. Наконец, вы можете переопределить это соглашение, установив defined_in: any_file_name_you_want — но мы настоятельно рекомендуем следовать соглашению, если у вас нет очень веской причины.

Где они будут материализованы? Каждая версия модели создаст отношение в базе данных с псевдонимом <model_name>_v<v>. В этом случае: dim_customers_v1 и dim_customers_v2. См. раздел ниже для получения более подробной информации о настройке псевдонимов.

Какая версия является "последней"? Если не указано явно, latest_version будет 2, потому что она численно больше. В этом случае мы явно указали, что latest_version: 1. Это означает, что v2 является "предрелизной", в ранней стадии разработки и тестирования. Когда мы будем готовы развернуть v2 для всех по умолчанию, мы повысим latest_version: 2 или удалим latest_version из спецификации.

Настройка версионированных моделей

Вы можете перенастроить каждую версию независимо. Например, вы могли бы материализовать v2 как таблицу, а v1 как представление:

models/schema.yml
versions:
- v: 2
config:
materialized: table
- v: 1
config:
materialized: view

Как и в случае с наследованием конфигурации, любые конфигурации, установленные внутри определения версионированной модели (файл .sql или .py), будут иметь приоритет над конфигурациями, установленными в YAML.

Настройка расположения базы данных с помощью alias

Следуя примеру, предположим, вы хотите, чтобы dim_customers_v1 продолжал заполнять таблицу базы данных с именем dim_customers. Это то, как таблица была названа ранее, и у вас может быть несколько других панелей или инструментов, ожидающих чтения ее данных из <dbname>.<schemaname>.dim_customers.

Вы могли бы использовать конфигурацию alias:

models/schema.yml
      - v: 1
config:
alias: dim_customers # оставьте v1 в его оригинальном расположении в базе данных

Шаблон, который мы рекомендуем: Создайте представление или клон таблицы с каноническим именем модели, которое всегда указывает на последнюю версию. Следуя этому шаблону, вы можете предложить такую же гибкость, как ref, даже если кто-то выполняет запросы вне dbt. Хотите конкретную версию? Закрепите на версии X, добавив суффикс _vX. Хотите последнюю версию? Без суффикса, и представление перенаправит вас.

Мы намерены встроить это в dbt-core как функциональность из коробки. (Проголосуйте или оставьте комментарий на dbt-core#7442.) В то же время, вы можете реализовать этот шаблон самостоятельно с помощью пользовательского макроса и post-hook:

macros/create_latest_version_view.sql
{% macro create_latest_version_view() %}

-- этот hook будет выполняться только если модель версионирована и только если это последняя версия
-- в противном случае, это no-op
{% if model.get('version') and model.get('version') == model.get('latest_version') %}

{% set new_relation = this.incorporate(path={"identifier": model['name']}) %}

{% set existing_relation = load_relation(new_relation) %}

{% if existing_relation and not existing_relation.is_view %}
{{ drop_relation_if_exists(existing_relation) }}
{% endif %}

{% set create_view_sql -%}
-- этот синтаксис может варьироваться в зависимости от платформы данных
create or replace view {{ new_relation }}
as select * from {{ this }}
{%- endset %}

{% do log("Создание представления " ~ new_relation ~ " указывающего на " ~ this, info = true) if execute %}

{{ return(create_view_sql) }}

{% else %}

-- no-op
select 1 as id

{% endif %}

{% endmacro %}
dbt_project.yml
# dbt_project.yml
models:
post-hook:
- "{{ create_latest_version_view() }}"
к сведению

Если ваш проект исторически реализовывал пользовательские псевдонимы путем повторной реализации макроса generate_alias_name, и вы хотите начать использовать версии моделей, вам следует обновить вашу пользовательскую реализацию, чтобы учитывать версии моделей. В частности, мы бы рекомендовали добавить условие, подобное этому.

Ваша существующая реализация generate_alias_name не должна столкнуться с ошибками при первом обновлении до v1.5. Только когда вы создадите свою первую версионированную модель, вы можете увидеть ошибку, подобную этой:

dbt.exceptions.AmbiguousAliasError: Ошибка компиляции
dbt нашел два ресурса с представлением базы данных "database.schema.model_name".
dbt не может создать два ресурса с идентичными представлениями базы данных. Чтобы исправить это,
измените конфигурацию одного из этих ресурсов:
- model.project_name.model_name.v1 (models/.../model_name.sql)
- model.project_name.model_name.v2 (models/.../model_name_v2.sql)

Мы выбрали использование generate_alias_name для этой функциональности, чтобы логика оставалась доступной для конечных пользователей и могла быть повторно реализована с пользовательской логикой.

Запуск модели с несколькими версиями

Чтобы запустить модель с несколькими версиями, вы можете использовать флаг --select. Например:

  • Запустите все версии dim_customers:

    dbt run --select dim_customers # Запустите все версии модели
  • Запустите только версию 2 dim_customers:

    Вы можете использовать любую из следующих команд (обе достигают одного и того же результата):

      dbt run --select dim_customers.v2 # Запустите конкретную версию модели
    dbt run --select dim_customers_v2 # Альтернативный синтаксис для конкретной версии
  • Запустите последнюю версию dim_customers с использованием сокращенного флага --select:

    dbt run -s dim_customers,version:latest # Запустите последнюю версию модели

Эти команды предоставляют гибкость в управлении и выполнении различных версий модели dbt.

Оптимизация версий моделей

Как вы определяете каждую версию модели, полностью зависит от вас. Хотя легко начать с копирования-вставки из одного определения модели SQL в другое, вы должны подумать о том, что на самом деле меняется от одной версии к другой.

Например, если ваша новая версия модели только переименовывает или удаляет определенные столбцы, вы могли бы определить одну версию как представление поверх другой:

models/dim_customers_v2.sql
{{ config(materialized = 'view') }}

{% set dim_customers_v1 = ref('dim_customers', v=1) %}

select
{{ dbt_utils.star(from=dim_customers_v1, except=["country_name"]) }}
from {{ dim_customers_v1 }}

Конечно, если одна версия модели вносит значительные и существенные изменения в логику другой, возможно, не удастся оптимизировать ее таким образом. В этом случае стоимость человеческой интуиции и читаемости важнее, чем стоимость повторного вычисления аналогичных преобразований.

Мы ожидаем разработать более обоснованные рекомендации, когда команды начнут применять версии моделей на практике. Один из рекомендуемых шаблонов, который мы можем представить: Приоритизируйте определение latest_version, и определяйте другие версии (старые и предрелизные) на основе их различий от последней. Как?

  • Определите свойства и конфигурацию для последней версии в YAML модели верхнего уровня, а различия для других версий ниже (через include/exclude)
  • Где возможно, определяйте другие версии как select преобразования, которые берут последнюю версию в качестве отправной точки
  • При повышении latest_version, мигрируйте SQL и YAML соответственно.

В приведенном выше примере третий пункт может быть сложным. Легче исключить country_name, чем добавить его обратно. Вместо этого нам, возможно, придется сохранить полную оригинальную логику для dim_customers_v1 — но материализовать ее как view, чтобы минимизировать стоимость построения в хранилище данных. Если downstream запросы видят немного ухудшенную производительность, это все равно значительно лучше, чем сломанные запросы, и еще одна причина для миграции на новую "последнюю" версию.

0