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

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

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

Слово «version» встречается в документации в нескольких местах и используется в разных значениях:

  • Model versions — функция dbt Mesh, которая обеспечивает более эффективное управление и контроль моделей данных, позволяя отслеживать изменения и обновления моделей с течением времени.
  • dbt_project.yml version (опционально) — версия dbt_project.yml не связана с Mesh и указывает на совместимость dbt-проекта с конкретной версией dbt.
  • .yml property file version (опционально) — номера версий в .yml-файлах свойств определяют, как dbt интерпретирует эти YAML-файлы. Не связано с Mesh.

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

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

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

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

Соображения

Есть несколько моментов, которые стоит учитывать при использовании возможностей управления моделями:

  • Такие функции управления моделями, как доступ к моделям (model access), контракты (contracts) и версии (versions), повышают уровень доверия и стабильности в вашем проекте dbt. Однако из‑за добавления дополнительной структуры они могут усложнить откат изменений (например, при необходимости убрать ограничения доступа к модели) и увеличить затраты на сопровождение, если внедрить их слишком рано.
    Перед тем как добавлять функции управления, стоит оценить, готов ли ваш проект dbt извлечь из них пользу. Внедрение управления на этапе, когда модели все еще активно меняются, может усложнить будущие изменения.

  • Функции управления применяются только к моделям. Они не распространяются на другие типы ресурсов, включая snapshots, seeds или sources. Это связано с тем, что такие объекты могут со временем менять свою структуру (например, snapshots фиксируют изменяющиеся исторические данные) и плохо подходят для предоставления гарантий вроде контрактов, управления доступом или версионирования.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как правило, в каждый момент времени в окружение развёртывается только одна версия кода проекта. Если что-то пойдёт не так, у вас есть возможность откатить изменения, вернув commit или pull request, либо воспользовавшись возможностями платформы данных, связанными с «time travel».

Когда вы вносите обновления в исходный код модели — ее логическое определение на 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
Loading table...

Как вы увидите в разделе реализации ниже, версионированная модель может повторно использовать большинство своих 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]

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

Конфигурация выше означает следующее: вместо двух несвязанных моделей у меня есть два версионированных определения одной и той же модели — 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‑пользователи запросов столкнутся с немного ухудшенной производительностью, это всё равно значительно лучше, чем сломанные запросы, и является дополнительным стимулом для миграции на новую «последнюю» версию.

Координация версионирования моделей

Безопасный выпуск новой версии модели требует координации между производителями моделей (которые их разрабатывают) и потребителями моделей (которые от них зависят).

Практические рекомендации о том, как производителям и потребителям следует общаться, тестировать и выкатывать версионированные модели между проектами, см. в разделе Coordinating model versions best practices.

Нашли ошибку?

0
Loading