Пользовательские функции (User-defined functions)
Пользовательские функции (UDF) позволяют определять и регистрировать собственные функции непосредственно в вашем хранилище данных. Подобно макросам, UDF способствуют повторному использованию кода, но в отличие от макросов они являются объектами в хранилище. Это означает, что одну и ту же логику можно использовать не только в dbt, но и в других инструментах — например, в BI-системах, ноутбуках для data science и т.д.
UDF особенно полезны для переиспользования логики в нескольких инструментах, стандартизации сложных бизнес‑расчётов, повышения производительности вычислительно тяжёлых операций (поскольку они компилируются и оптимизируются движком запросов вашего хранилища), а также для контроля версий кастомной логики внутри вашего dbt‑проекта.
dbt создаёт, обновляет и переименовывает UDF в процессе выполнения DAG. UDF создаётся в хранилище до той модели, которая на него ссылается. Подробнее о том, как dbt строит UDF в проекте, см. listing and building UDFs.
Дополнительную информацию о конфигурациях и свойствах UDF см. в разделах Function properties и Function configurations.
Предварительные требования
-
Убедитесь, что вы используете v2 Stable или v1 Latest канала выпуска платформы dbt либо dbt v1.11+.
-
Используйте один из следующих адаптеров:
- dbt v1
- dbt v2
- BigQuery
- Snowflake
- Redshift
- Postgres
- Databricks
- Starburst/Trino (
dbt-trinov1.11+)
- BigQuery
- Snowflake
- Redshift
- Databricks
UDF на JavaScript поддерживаются в dbt v1.12 и выше на Snowflake и BigQuery.
Другие языки (например, Java, Scala) в настоящее время не поддерживаются для UDF.
Полный список поддерживаемых возможностей UDF см. в разделе Limitations ниже.
Определение UDF в dbt
В dbt можно определять UDF на SQL, Python и JavaScript (доступны в dbt v1.12+). UDF на Python поддерживаются в Snowflake, BigQuery и Databricks (требуется Unity Catalog). UDF на JavaScript поддерживаются в Snowflake и BigQuery.
Выполните следующие шаги, чтобы определить UDF в dbt:
-
Создайте файл SQL, Python или JavaScript в каталоге
functions. Например, эта UDF проверяет, представляет ли строка положительное целое число:- SQL
- Python
- JavaScript
Определение SQL UDF в SQL‑файле.
functions/is_positive_int.sql# syntax for BigQuery, Snowflake, and Databricks
REGEXP_INSTR(a_string, '^[0-9]+$')
# syntax for Redshift and Postgres
SELECT REGEXP_INSTR(a_string, '^[0-9]+$')
# syntax for Starburst/Trino
CASE WHEN regexp_like(a_string, '^[0-9]+$') THEN 1 ELSE 0 ENDОпределите Python UDF в файле Python.
functions/is_positive_int.pyimport re
def main(a_string):
return 1 if re.search(r'^[0-9]+$', a_string or '') else 0В Databricks содержимое файла
.pyиспользуется как тело UDF без изменений. Databricks выполняет его напрямую, без вызова именованной точки входа. Последней инструкцией тела должен бытьreturnверхнего уровня, который возвращает результат. Из-за такогоreturnисходник Databricks представляет собой тело функции, а не исполняемый модуль.py. Например:functions/is_positive_int.pyimport re
def main(a_string):
return 1 if re.search(r'^[0-9]+$', a_string or '') else 0
return main(a_string)Примечание: Для Python UDF в Databricks требуется Unity Catalog.
Определите JavaScript UDF в файле JavaScript.
functions/is_positive_int.jsreturn /^[0-9]+$/.test(a_string) ? 1 : 0;
Примечание: Вы можете указать конфигурации в блоке config в SQL-файле или в соответствующем YAML-файле свойств на шаге 2.
-
Укажите имя функции и определите конфигурацию, свойства, тип возвращаемого значения и необязательные аргументы в соответствующем YAML-файле свойств.
- SQL
- Python
- JavaScript
functions/is_positive_int.ymlfunctions:
- name: is_positive_int # required
description: My UDF that returns 1 if a string represents a naked positive integer (like "10", "+8" is not allowed). # optional
config:
schema: udf_schema
database: udf_db
volatility: deterministic
arguments: # optional
- name: a_string # required if arguments is specified
data_type: string # required if arguments is specified
description: The string that I want to check if it's representing a positive integer (like "10")
default_value: "'1'" # optional, available in Snowflake and Postgres
returns: # required
data_type: integer # requiredДля Starburst/Trino укажите в
data_typeаргумента тип данных, который поддерживает движок, напримерvarcharвместоstring. Не задавайтеdefault_value. В Starburst Galaxy задайтеdatabase: galaxyиschema: functions.Следующие конфигурации обязательны при определении Python UDF в Snowflake и BigQuery:
runtime_version— версия Python, в которой будет выполняться код. Поддерживаемые значения:entry_point— Укажите функцию Python, которую нужно вызвать.
В Databricks параметры
runtime_versionиentry_pointпринимаются для совместимости между адаптерами, но ни на что не влияют. Databricks самостоятельно управляет средой выполнения Python и напрямую использует тело функции, поэтому dbt выводит предупреждение, если вы задаёте эти параметры.С помощью необязательной настройки
packagesможно подключить к Python UDF общедоступные пакеты из PyPI. Укажите имена пакетов, напримерnumpyиpandas. При необходимости закрепите версии, напримерpandas==1.5.0. Хранилище установит пакеты при создании UDF. В Snowflake некоторые пакеты устанавливаются из репозитория Anaconda. Для их использования может потребоваться принять условия обслуживания Anaconda.В примере Python UDF заданы обязательные параметры
runtime_versionиentry_point, необязательный параметрpackagesи другие распространённые настройки:functions/is_positive_int.ymlfunctions:
- name: is_positive_int # обязательно
description: Моя UDF, которая возвращает 1, если строка представляет собой «чистое» положительное целое число (например, "10"; "+8" не допускается). # optional
config:
runtime_version: "3.11" # обязательно для Snowflake и BigQuery; необязательно и игнорируется в Databricks
entry_point: main # обязательно для Snowflake и BigQuery; необязательно и игнорируется в Databricks
packages: # необязательно, только для Python UDF
- numpy
- pandas==1.5.0
schema: udf_schema
database: udf_db
volatility: deterministic
arguments: # опционально
- name: a_string # обязательно, если указан arguments
data_type: string # обязательно, если указан arguments
description: Строка, по которой я хочу проверить, что она представляет положительное целое число (например, "10")
default_value: "'1'" # опционально; доступно в Snowflake и Postgres
returns: # обязательно
data_type: integer # обязательноПараметр
snowflake.quote_argsпозволяет указать, следует ли заключать имена аргументов в кавычки при создании JavaScript UDF в Snowflake.functions/is_positive_int.ymlfunctions:
- name: is_positive_int # required
description: My UDF that returns 1 if a string represents a naked positive integer (like "10", "+8" is not allowed). # optional
config:
snowflake: # optional
quote_args: true # optional, JavaScript UDFs on Snowflake only
arguments: # optional
- name: a_string # required if arguments is specified
data_type: string # required if arguments is specified
description: The string to check # optional
returns: # required
data_type: integer # required
В dbt параметр volatility принимается для UDF на SQL, Python и JavaScript, но его обработка зависит от хранилища данных. Для UDF на SQL и Python в BigQuery параметр volatility игнорируется, а dbt выводит предупреждение. Для UDF на JavaScript в BigQuery значения deterministic и non-deterministic применяются при создании UDF; значение stable не поддерживается. В Snowflake все поддерживаемые значения volatility применяются при создании UDF. Дополнительную информацию см. в разделе volatility.
-
Выполните одну из следующих команд
dbt build, чтобы собрать UDF и создать их в хранилище:Сборка всех UDF:
dbt build --select "resource_type:function"Или сборка конкретной UDF:
dbt build --select is_positive_int
При выполнении dbt build файл свойств (functions/is_positive_int.yml) и соответствующий файл SQL, Python или JavaScript совместно используются для формирования инструкции CREATE FUNCTION.
Сформированная инструкция CREATE FUNCTION зависит от используемого адаптера. Например:
- SQL
- Python
- JavaScript
- Snowflake
- Redshift
- BigQuery
- Databricks
- Postgres
- Starburst/Trino
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string STRING DEFAULT '1')
RETURNS INTEGER
LANGUAGE SQL
IMMUTABLE
AS $$
REGEXP_INSTR(a_string, '^[0-9]+$')
$$;
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string VARCHAR)
RETURNS INTEGER
IMMUTABLE
AS $$
SELECT REGEXP_INSTR(a_string, '^[0-9]+$')
$$ LANGUAGE SQL;
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string STRING)
RETURNS INT64
AS (
REGEXP_INSTR(a_string, r'^[0-9]+$')
);
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string STRING)
RETURNS INT
DETERMINISTIC
RETURN REGEXP_INSTR(a_string, '^[0-9]+$');
CREATE OR REPLACE FUNCTION udf_schema.is_positive_int(a_string text DEFAULT '1')
RETURNS int
LANGUAGE sql
IMMUTABLE
AS $$
SELECT regexp_instr(a_string, '^[0-9]+$')
$$;
CREATE OR REPLACE FUNCTION galaxy.functions.is_positive_int(a_string VARCHAR)
RETURNS INTEGER
DETERMINISTIC
COMMENT 'My UDF that returns 1 if a string represents a naked positive integer (like "10", "+8" is not allowed).'
RETURN CASE WHEN regexp_like(a_string, '^[0-9]+$') THEN 1 ELSE 0 END;
Подробнее см. раздел Настройки Starburst/Trino.
- Snowflake
- BigQuery
- Databricks
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string STRING DEFAULT '1')
RETURNS INTEGER
LANGUAGE PYTHON
RUNTIME_VERSION = '3.11'
HANDLER = 'main'
PACKAGES = ('numpy', 'pandas==1.5.0')
AS $$
import re
def main(a_string):
return 1 if re.search(r'^[0-9]+$', a_string or '') else 0
$$;
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string STRING)
RETURNS INT64
LANGUAGE python
OPTIONS(
runtime_version = "python-3.11",
entry_point = "main",
packages = ['numpy', 'pandas==1.5.0']
)
AS r'''
import re
def main(a_string):
return 1 if re.search(r'^[0-9]+$', a_string or '') else 0
''';
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string STRING)
RETURNS INT
LANGUAGE PYTHON
AS $$
import re
def main(a_string):
return 1 if re.search(r'^[0-9]+$', a_string or '') else 0
return main(a_string)
$$;
Databricks не добавляет выражения RUNTIME_VERSION и HANDLER. Средой выполнения управляет Databricks. Содержимое файла .py становится телом функции без изменений, включая завершающую инструкцию return main(a_string), которая возвращает результат.
- Snowflake
- BigQuery
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int("a_string" STRING)
RETURNS INTEGER
LANGUAGE JAVASCRIPT
AS $$
return /^[0-9]+$/.test(a_string) ? 1 : 0;
$$;
CREATE OR REPLACE FUNCTION udf_db.udf_schema.is_positive_int(a_string STRING)
RETURNS INT64
LANGUAGE js
AS r'''
return /^[0-9]+$/.test(a_string) ? 1 : 0;
''';
-
Используйте UDF в модели с помощью макроса
{{ function(...) }}. Например:models/my_model.sqlselect
maybe_positive_int_column,
{{ function('is_positive_int') }}(maybe_positive_int_column) as is_positive_int
from {{ ref('a_model_i_like') }}
При использовании --defer function() ссылается на существующую UDF в среде, на которую перенаправляются ссылки (например, в production), если функция не выбрана или ещё не создана в вашей целевой среде. Для этого требуется манифест состояния, указанный с помощью --state или эквивалентной переменной окружения (например, DBT_ENGINE_STATE), который dbt использует, чтобы определить, куда перенаправлять ссылки. Это позволяет успешно запускать модели, зависящие от UDF, в рабочих процессах непрерывной интеграции и разработки. Подробнее см. в разделе Настройка выбора на основе состояния.
-
Выполните
dbt compile, чтобы увидеть, как формируется ссылка на UDF. В следующем примере{{ function('is_positive_int') }}заменяется именем UDFudf_db.udf_schema.is_positive_int.models/my_model.sqlselect
maybe_positive_int_column,
udf_db.udf_schema.is_positive_int(maybe_positive_int_column) as is_positive
from analytics.dbt_schema.a_model_i_likeВ DAG создаётся узел UDF на основе SQL/Python‑файла и YAML‑описания, и появляется зависимость
is_positive_int→my_model.
После определения UDF ваши изменения применяются к UDF в хранилище при следующем запуске dbt build, если вы изменили что-либо из следующего:
- Файл SQL, Python или JavaScript, содержащий тело функции (
is_positive_int.sql,is_positive_int.pyилиis_positive_int.jsв этих примерах) - Её конфигурации
- Её свойства, определённые в файле
.yml(например,argumentsилиreturns)
dbt обнаруживает все эти изменения при использовании state:modified.
Определение перегруженных UDF
Используйте свойство overloads (доступно в dbt v1.12+), чтобы определить несколько сигнатур аргументов для одной функции. Это позволяет вызывать функцию с одним и тем же именем, передавая входные данные разных типов, без создания отдельных UDF для каждого варианта. overloads поддерживается для SQL UDF в Snowflake и Postgres, а также для UDF на Python и JavaScript в Snowflake.
Чтобы определить перегруженные UDF:
-
Добавьте список
overloadsв определение функции в YAML-файле свойств. Каждая запись используетdefined_inдля ссылки на отдельный файл и может содержать необязательные свойстваargumentsиreturns:functions/is_positive_int.ymlfunctions:
- name: is_positive_int
arguments:
- name: a_string
data_type: string
returns:
data_type: integer
overloads:
- defined_in: is_positive_int_numeric # references functions/is_positive_int_numeric.sql
arguments:
- name: a_num
data_type: numeric
returns: # optional, inherits from root function if omitted
data_type: integer -
Создайте отдельный файл для тела каждой перегрузки.
Например, тело основной функции, принимающей аргумент типа
string:functions/is_positive_int.sql# Snowflake syntax
REGEXP_INSTR(a_string, '^[0-9]+$')И тело перегрузки, принимающей аргумент типа
numeric:functions/is_positive_int_numeric.sql# Snowflake syntax
CASE WHEN a_num > 0 THEN 1 ELSE 0 END
Все перегрузки объединяются в один узел DAG (основную функцию), поэтому они создаются и выбираются вместе. При повторной попытке dbt пропускает успешно созданные перегрузки и повторно запускает только те, при создании которых произошла ошибка. При создании функции dbt формирует отдельный оператор CREATE FUNCTION для каждой перегрузки, используя одно и то же имя функции, но разные типы аргументов.
Дополнительную информацию см. в разделе overloads.
Использование UDF в unit‑тестах
Вы можете использовать unit tests для проверки моделей, которые ссылаются на UDF. Перед запуском unit‑тестов убедитесь, что функция существует в хранилище. Чтобы гарантировать это для unit‑теста, выполните:
dbt build --select "+my_model_to_test" --empty
Следуя примеру из раздела Определение UDF в dbt, ниже приведён пример unit‑теста, который проверяет модель, вызывающую UDF:
unit_tests:
- name: test_is_positive_int
description: "Проверьте, что моя логика is_positive_int учитывает крайние случаи"
model: my_model
given:
- input: ref('a_model_i_like')
rows:
- { maybe_positive_int_column: 10 }
- { maybe_positive_int_column: -4 }
- { maybe_positive_int_column: +8 }
- { maybe_positive_int_column: 1.0 }
expect:
rows:
- { maybe_positive_int_column: 10, is_positive: true }
- { maybe_positive_int_column: -4, is_positive: false }
- { maybe_positive_int_column: +8, is_positive: true }
- { maybe_positive_int_column: 1.0, is_positive: true }
Просмотр и сборка UDF
Используйте команду list, чтобы вывести список UDF в проекте: dbt list --select "resource_type:function" или dbt list --resource-type function.
Используйте команду build, чтобы выбирать UDF при сборке проекта: dbt build --select "resource_type:function".
Подробнее о выборе UDF см. в примерах в разделе Node selector methods.
Ограничения
- UDF на других языках (например, Java или Scala) пока не поддерживаются.
- UDF на JavaScript поддерживаются только в Snowflake и BigQuery. Использование UDF на JavaScript с неподдерживаемым адаптером вызывает ошибку разбора.
- UDF на Python поддерживаются только в Snowflake, BigQuery и Databricks (при использовании dbt v1 или dbt v2). Другие хранилища данных пока не поддерживаются для UDF на Python.
- В настоящее время поддерживаются только скалярные и агрегатные функции. Подробнее см. в разделе Поддерживаемые типы функций.
- Свойство
overloadsподдерживается для UDF на SQL в Snowflake и Postgres, а также для UDF на Python и JavaScript в Snowflake.
