Learning Platform
Глоссарий Troubleshooting
Урок 12.06 · 22 мин
Средний
pythonudfregisterscalar-functions

Регистрация Python-объектов и UDF

Replacement scan делает Python-переменные видимыми в SQL автоматически. Но иногда автоматики мало: объект создан без именованной переменной, имя нужно другое, или — самое интересное — в SQL-запросе нужно вызвать свою функцию на Python. Этот завершающий урок модуля про два инструмента ручного управления: register для явной привязки объектов и Python UDF — пользовательские функции, которые приносят Python-код прямо внутрь SQL-запроса DuckDB.

register: явная привязка объекта

DataFusion Python: UDF, UDAF и UDWF ClickHouse: SQL UDF и Executable UDF

Метод con.register(name, object) создаёт в каталоге соединения именованную ссылку на Python-объект. После него объект доступен в SQL под заданным именем — независимо от того, как называется (и называется ли вообще) Python-переменная.

import duckdb
import pandas as pd

con = duckdb.connect()
df = pd.DataFrame({"product": ["A", "B"], "price": [100, 250]})

# Явно привязываем объект к имени catalog в SQL
con.register("catalog", df)
con.sql("SELECT * FROM catalog")

Зачем это, если есть автоматический replacement scan? В трёх случаях:

  • Имя в SQL не должно зависеть от имени переменной. Replacement scan требует совпадения. register разрывает связь: переменная может называться как угодно, в SQL — фиксированное имя. Это важно для кода, который генерирует запросы.
  • У объекта нет именованной переменной. DataFrame создан выражением, лежит в списке, пришёл аргументом функции — replacement scan не за что зацепиться. register даёт ему имя явно.
  • Контроль над временем жизни привязки. register создаёт привязку до явного unregister или закрытия соединения; replacement scan ищет переменную заново при каждом запросе.
# DataFrame без именованной переменной — replacement scan бессилен
con.register("first_batch", build_dataframe()[0])
con.sql("SELECT COUNT(*) FROM first_batch")

# Снять привязку, когда объект больше не нужен
con.unregister("first_batch")
register: явная ссылка в каталоге соединения
Python-объектDataFrame, Arrow Table — возможно без именованной переменной или с произвольным именем
con.register('name', obj)
Именованная ссылка в каталогеВ каталоге соединения появляется фиксированное имя, указывающее на объект; не зависит от имени переменной
запрос по имени
SELECT ... FROM nameSQL обращается к объекту по заданному имени, пока привязка жива

register привязывает данные. Вторая, более мощная возможность — привязать код.

Python UDF: своя функция внутри SQL

SQL DuckDB богат встроенными функциями, но Python-экосистема богаче на порядки: библиотеки для текста, дат, геокодирования, ML-инференса, доменной логики. UDF (user-defined function, пользовательская функция) позволяет вызвать Python-функцию прямо из SQL-запроса. Вы пишете обычную функцию на Python, регистрируете её — и она становится SQL-функцией, как upper или abs.

Регистрация — метод con.create_function(name, func, argument_types, return_type):

import duckdb

con = duckdb.connect()

# Обычная Python-функция
def tax(amount: float) -> float:
    return round(amount * 0.2, 2)

# Регистрируем её как SQL-функцию
con.create_function("tax", tax, ["DOUBLE"], "DOUBLE")

# Теперь tax() доступна в SQL наравне со встроенными
con.sql("SELECT product, price, tax(price) AS vat FROM (VALUES ('A', 100.0), ('B', 250.0)) t(product, price)")
┌─────────┬────────┬────────┐
│ product │ price  │  vat   │
│ varchar │ double │ double │
├─────────┼────────┼────────┤
│ A       │ 100.0  │  20.0  │
│ B       │ 250.0  │  50.0  │
└─────────┴────────┴────────┘

При регистрации указываются: имя функции в SQL, сама Python-функция, список типов аргументов и тип возвращаемого значения — DuckDB должен знать сигнатуру, чтобы встроить функцию в типизированный план запроса. Часто типы можно вывести из аннотаций Python, но явное указание надёжнее.

UDF особенно ценны там, где встроенного SQL не хватает: применить Python-библиотеку нормализации текста, дернуть ML-модель для скоринга каждой строки, посчитать доменную метрику со сложной логикой. Функция пишется на привычном Python, а вызывается декларативно в SQL.

# UDF, использующая Python-библиотеку, которой нет в SQL
import hashlib

def sha256_hex(s: str) -> str:
    return hashlib.sha256(s.encode()).hexdigest()

con.create_function("sha256_hex", sha256_hex, ["VARCHAR"], "VARCHAR")
con.sql("SELECT sha256_hex(email) AS email_hash FROM users")

Цена UDF: производительность

Здесь — главное предостережение урока. Python UDF удобны, но они медленнее встроенных функций DuckDB, и понимать почему — обязательно.

Встроенная функция DuckDB векторизована: она написана на C++ и обрабатывает весь DataChunk — батч на ~2048 значений — за один вызов, без накладных расходов на каждое значение. Python UDF в простейшей форме вызывается построчно: для каждого значения колонки DuckDB передаёт управление в интерпретатор Python, вызывает функцию, забирает результат. На миллионах строк это миллионы переходов между движком и интерпретатором плюс оверхед самого интерпретатора Python. Векторизованный конвейер при этом фактически разрывается на каждом значении.

Встроенная функция против построчной Python UDF
Встроенная функция (C++)Обрабатывает весь DataChunk на ~2048 значений за один вызов, векторизованно, без оверхеда на значение
vs
Построчная Python UDFДля каждого значения — переход в интерпретатор Python и обратно; миллион строк — миллион переходов

Есть способ смягчить эту цену. DuckDB поддерживает векторизованные Python UDF: функция-UDF получает не одно значение, а сразу вектор значений (как PyArrow-массив) и должна вернуть вектор. Накладные расходы на переход амортизируются по всему батчу, а внутри функции можно работать с массивом целиком средствами PyArrow или NumPy — векторно, без Python-цикла. Это резко ближе по скорости к встроенным функциям. Тип UDF задаётся при регистрации (через параметр, выбирающий построчный или Arrow-векторизованный режим).

АспектВстроенная функцияПострочная Python UDFВекторизованная Python UDF
ЯзыкC++PythonPython (+ PyArrow/NumPy)
Гранулярность вызоваВесь батчОдно значениеВесь батч (вектор)
СкоростьМаксимальнаяНизкаяБлизка к встроенной
Когда применятьВсегда, если естьПростая логика, малые данныеPython-логика на больших данных
WARNING

Не оборачивайте в Python UDF то, что DuckDB уже умеет сам. Арифметика, конкатенация строк, работа с датами, регулярные выражения, JSON — для всего этого есть быстрые встроенные функции. UDF на чистом SQL-выражении — это потеря производительности без всякой выгоды. Сначала проверьте, нет ли встроенной функции или возможности выразить логику обычным SQL; UDF берите только когда нужна именно Python-логика или Python-библиотека.

TIP

Если логику можно выразить SQL-выражением, но хочется переиспользуемое именованное выражение — это не повод для UDF. Используйте CREATE MACRO: макрос подставляется в запрос как SQL-выражение и исполняется векторизованным движком на полной скорости, в отличие от Python UDF. Python UDF нужны именно тогда, когда требуется выполнить Python-код, а не переиспользовать SQL.

Как это собирается с остальным модулем

Подведём черту под Python-экосистемой DuckDB. Replacement scan и zero-copy (уроки 1-2) дают автоматический бесшовный мост данных. Relational API (урок 3) — построение запросов методами Python. Ингест и экспорт (урок 4) — явные методы переноса данных. Инструменты (урок 5) — DuckDB как встроенный движок. И этот урок — ручное управление: register для явной привязки данных, UDF для внесения Python-кода в SQL.

Вместе это делает границу между SQL-миром и Python-миром почти прозрачной в обе стороны: данные ходят туда-сюда без копий, запросы строятся хоть строкой, хоть методами, а недостающую функциональность SQL можно дописать на Python. DuckDB не «ещё одна библиотека рядом с Pandas» — это SQL-движок, вплавленный в Python data-стек.

Попробуй сам

Для задания: pip install duckdb pandas pyarrow.

  1. Создайте DataFrame как элемент списка (без отдельной именованной переменной). Убедитесь, что replacement scan его не видит. Затем привяжите через con.register("data", список[0]) и запросите. Снимите привязку через unregister.
  2. Напишите простую Python-функцию (например классификацию числа на «low/mid/high»), зарегистрируйте через create_function и вызовите в SQL-запросе над таблицей.
  3. Сгенерируйте таблицу на несколько миллионов строк. Примените к колонке построчную Python UDF и измерьте время. Затем сделайте то же самое встроенным SQL-выражением (если логика выразима) и сравните. Прочувствуйте разницу в скорости.
  4. Сформулируйте для себя правило выбора: когда нужна Python UDF, когда хватит встроенной функции, а когда лучше CREATE MACRO. Опирайтесь на то, нужен ли именно Python-код и каков объём данных.

DataFusion: UDF на Rust — векторизованные функции без оверхеда
Проверка знанийKnowledge check
Зачем нужен метод register, если есть автоматический replacement scan, и почему построчная Python UDF медленнее встроенной функции DuckDB?
ОтветAnswer
Метод con.register(name, object) создаёт в каталоге соединения именованную ссылку на Python-объект, после чего объект доступен в SQL под заданным именем. Он нужен в трёх случаях, которые автоматический replacement scan не покрывает. Первый: имя в SQL не должно зависеть от имени переменной — replacement scan требует их буквального совпадения, а register разрывает эту связь, что важно для кода, генерирующего запросы. Второй: у объекта нет именованной переменной — DataFrame создан выражением, лежит в списке или пришёл аргументом функции, и replacement scan не за что зацепиться, а register даёт объекту имя явно. Третий: контроль над временем жизни привязки — register создаёт привязку до явного unregister или закрытия соединения, тогда как replacement scan ищет переменную заново при каждом запросе. Построчная Python UDF медленнее встроенной функции DuckDB из-за разницы в гранулярности вызова. Встроенная функция DuckDB написана на C++ и векторизована — она обрабатывает весь DataChunk, батч примерно на 2048 значений, за один вызов, без накладных расходов на каждое отдельное значение. Построчная Python UDF в простейшей форме вызывается для каждого значения колонки отдельно: для каждого значения DuckDB передаёт управление в интерпретатор Python, вызывает функцию и забирает результат. На миллионах строк это миллионы переходов между движком и интерпретатором плюс оверхед самого интерпретатора Python, а векторизованный конвейер фактически разрывается на каждом значении. Смягчить эту цену позволяют векторизованные Python UDF: функция получает сразу вектор значений (как PyArrow-массив) и возвращает вектор, накладные расходы на переход амортизируются по всему батчу, а внутри можно работать с массивом целиком средствами PyArrow или NumPy без Python-цикла — это резко ближе к скорости встроенных функций. Поэтому не стоит оборачивать в Python UDF то, что DuckDB умеет сам: для арифметики, строк, дат, JSON есть быстрые встроенные функции, а для переиспользуемых SQL-выражений — CREATE MACRO, который исполняется векторизованно. Python UDF нужны именно тогда, когда требуется выполнить Python-код или использовать Python-библиотеку, которой нет в SQL.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В каком случае нужен метод con.register, хотя есть автоматический replacement scan?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6