Skip to main content
Пользовательские функции (UDF) позволяют расширять возможности ClickHouse сверх того, что доступно в более чем тысяче готовых функций. В ClickHouse Cloud есть несколько способов создавать пользовательские функции и управлять ими:
  1. С помощью SQL
  2. С помощью консоли и собственного кода
  3. С помощью Cloud API (бета)
  4. С помощью Terraform (бета)
Исполняемые UDF доступны в AWS, GCP и Azure. Они уже включены для всех организаций, и ничего дополнительно активировать не требуется. Отдельно они не тарифицируются: процессы UDF выполняются внутри подов сервиса и используют его собственные ресурсы CPU и памяти.

Пользовательские функции SQL

Пользовательские функции SQL можно создавать с помощью оператора CREATE FUNCTION на основе лямбда-выражения. В этом примере мы создадим простую исполняемую пользовательскую функцию isBusinessHours. Функция будет проверять, попадает ли указанная временная метка в стандартные рабочие часы, и возвращать true, если да, и false — если нет.
  1. Войдите в Cloud Console и откройте консоль SQL
  2. Напишите следующий SQL-запрос, чтобы создать функцию isBusinessHours:
  1. Чтобы протестировать только что созданную UDF, выполните следующую команду:
Вы должны получить такой результат:
  1. Вы можете использовать команду DROP FUNCTION, чтобы удалить только что созданную UDF:
Важнопользовательская функция (UDF) в ClickHouse Cloud не наследует настройки на уровне пользователя. Она выполняется с системными настройками по умолчанию.
Это означает:
  • Настройки уровня сеанса (заданные через оператор SET) не передаются в контекст выполнения UDF
  • Настройки профиля пользователя не наследуются UDF
  • Настройки уровня запроса не применяются при выполнении UDF

Исполняемые пользовательские функции

Исполняемые UDF запускают ваш код в изолированном процессе (песочнице) рядом с каждой репликой вашего сервиса. Можно загрузить скрипт на Python, который выполняется в среде python3.11, или заранее скомпилированный бинарный файл, который выполняется в нативной среде выполнения. В этом примере мы создадим ту же простую исполняемую пользовательскую функцию isBusinessHours, которая проверяет, попадает ли заданная временная метка в обычные рабочие часы. Ранее мы создавали её с помощью SQL, а на этот раз создадим её с помощью Python и настроим через консоль.
1

Создайте файл Python

Создайте локально новый файл main.py:
Среда выполнения Python — python3.11. Если ваш Python-скрипт импортирует сторонние пакеты, перечислите их в файле requirements.txt, и ClickHouse Cloud установит их автоматически для обеих архитектур CPU, на которых работает ClickHouse Cloud (x86-64 и ARM64). Зависимости можно также упаковать непосредственно в ZIP-архив, но в этом случае придётся включить в него кэшированные пакеты для обеих архитектур CPU, поэтому вариант с requirements.txt проще. Например:
ClickHouse Cloud ищет файл main.py в zip-архиве, который вы загрузите через консоль на следующем шаге. Если назвать файл иначе, возникнет ошибка.
2

Упакуйте зависимости и локальные файлы

В этом руководстве нет зависимостей и дополнительных файлов, поэтому в архив достаточно поместить только main.py:
Чтобы добавить пакеты зависимостей и другие локальные файлы (например, requirements.txt, wheel-пакеты, файлы конфигурации или файлы данных), поместите их в тот же каталог, где находится main.py, а затем выполните zip из этого каталога, чтобы main.py оказался в корне архива:
Чтобы сослаться на базовый каталог локального пакета в коде на Python, используйте os.path.dirname(os.path.abspath(__file__)). Это выражение возвращает абсолютный путь к каталогу внутри ZIP-архива, в котором находится ваш main.py, что позволяет обращаться к другим файлам пакета:
Это полезно, когда необходимо:
  • Обращаться к файлам конфигурации, поставляемым вместе с вашей UDF
  • Загружать wheel-пакеты для пользовательских зависимостей
  • Использовать дополнительные скрипты или файлы данных
Символические ссылки не допускаютсяClickHouse Cloud отклоняет архивы UDF, содержащие символические ссылки. Убедитесь, что ваш ZIP-архив содержит только обычные файлы и каталоги: архивы с символическими ссылками не пройдут проверку при загрузке.
3

Создайте UDF в консоли

  1. На главной странице Cloud console нажмите на название своей организации в меню в левом нижнем углу.
  2. Выберите в меню пункт User-defined functions.
  3. На странице пользовательских функций нажмите Set up a UDF. В правой части экрана откроется панель конфигурации.
  4. Введите имя функции. В этом примере используйте isBusinessHours.
  5. Выберите тип функции — Executable pool или Executable:
    • Executable pool: пул долгоживущих процессов запускается один раз и повторно используется для разных блоков и запросов. Рекомендуется практически для любой рабочей нагрузки.
    • Executable: для каждого обрабатываемого блока данных запускается новый процесс. Используйте этот вариант, только если процесс не должен сохранять состояние между вызовами.
  6. Выберите Runtime type. В этом примере оставьте python3.11. Чтобы загрузить предварительно скомпилированный бинарный файл, выберите Native; подробнее см. в разделе Среда выполнения Native.
  7. В этом примере оставьте настройки по умолчанию. Полный список параметров конфигурации и их значений по умолчанию в Cloud см. в разделе Справочник по конфигурации.
  8. Нажмите Browse File и загрузите файл .zip, созданный в начале этого руководства.
  9. Добавьте новый аргумент. В этом примере добавьте аргумент timestamp с типом DateTime.
  10. Выберите тип возвращаемого значения. В этом примере выберите Bool.
  11. Нажмите Create UDF. В диалоговом окне отобразится текущий статус сборки.
    • Если возникнут проблемы, статус изменится на error.
    • В противном случае статус сменится с building на provisioning. Чтобы подготовка завершилась, сервис должен быть активен. Если сервис находится в режиме простоя, нажмите Wake Up Service на панели UDF details рядом с названием сервиса.
    • По завершении статус изменится на deployed.
4

Протестируйте свою UDF

  1. вернитесь на главную страницу SQL Console, нажав Settings - return to your service view в левом верхнем углу страницы
  2. выберите SQL Console в меню слева
  3. введите следующий запрос:
Должен получиться следующий результат:
5

Создать новую версию

Чтобы изменить код или настройки UDF, создайте новую версию: существующие версии изменить нельзя. Панель Edit позволяет управлять только тем, каким сервисам назначена UDF; загрузка файла через эту панель не заменит развернутый код.
  1. На главной странице Cloud console нажмите на название вашей организации в меню в левом нижнем углу.
  2. Выберите в меню User-defined functions.
  3. Нажмите на три точки в столбце Actions для UDF isBusinessHours и выберите Create new version
  4. Загрузите zip-архив с измененным кодом или измените настройки, выберите сервисы, которые должны получить новую версию, и нажмите Create new version
Новая версия не наследует привязки к сервисам от предыдущей. На панели создания версии есть выбор сервисов, поэтому можно сначала протестировать версию на одном сервисе, а затем обновлять или откатывать версию для каждого сервиса по отдельности. Сервису в каждый момент времени может быть назначена только одна версия функции.Для UDF типа executable_pool долгоживущие процессы пула продолжают выполнять предыдущую версию, пока пул не будет перезагружен. Воспользуйтесь действием Reload UDF рядом с сервисом на панели UDF. Оно выполняет на этом сервисе команду SYSTEM RELOAD FUNCTION ON CLUSTER default isBusinessHours.Вы успешно добавили свою первую пользовательскую функцию через консоль, убедились, что она работает, и узнали, как при необходимости создать ее новую версию.

Нативная среда выполнения (скомпилированные бинарные файлы)

Нативная среда выполнения позволяет загрузить предварительно скомпилированный бинарный файл вместо скрипта на Python. При создании UDF выберите для параметра тип среды выполнения значение Native (runtime: native в API). В песочнице ничего не устанавливается автоматически: всё, что нужно бинарному файлу, должно быть встроено в него при компиляции. На данный момент поддерживаются Rust, Go, C++, а также JavaScript, скомпилированный в бинарный файл с помощью Bun. Работа с Bun проверена на версиях 1.4.0 и 1.4.2; с другими версиями возможны проблемы. Архив должен содержать по одному исполняемому файлу для каждой архитектуры CPU, каждый в отдельной папке:
Нужны обе архитектуры, поскольку сервисы ClickHouse Cloud работают на обеих. Исполняемый файл должен называться строго main и запускается без аргументов; режим или подкоманду передавайте как обычный аргумент функции. Настоятельно рекомендуется использовать статически скомпонованные бинарные файлы: они не зависят от системных библиотек хоста, системные вызовы которых могут различаться в разных версиях сервера, а политика системных вызовов песочницы настроена именно под них. Динамически скомпонованные бинарные файлы работают, только если они компонуются исключительно с glibc и собраны с glibc версии 2.35 или более ранней. JavaScript, скомпилированный с помощью Bun, всегда компонуется динамически.
Определяйте пути к файлам данных относительно исполняемого файлаФайлы из каталога архитектуры развертываются рядом с бинарным файлом, однако процесс запускается не в этом каталоге. Определяйте пути к ним относительно пути самого исполняемого файла, а не рабочего каталога: в Go используйте filepath.Dir(os.Args[0]) или os.Executable(), в Rust — std::env::current_exe(). Если обязательный файл отсутствует, выведите сообщение в stderr и завершите работу с ненулевым кодом, а не переключайтесь молча на запасной вариант.
Нативным UDF нельзя предоставить доступ к сети: исходящие сетевые подключения доступны только в среде выполнения Python. См. раздел Сетевой доступ. Соберите бинарный файл для обеих архитектур. В Rust один раз добавьте обе целевые платформы, а затем выполните сборку для каждой из них:
На Go:
Перед упаковкой папок в zip-архив скопируйте каждый собранный бинарный файл в amd64/main и arm64/main. Протокол обмена данными такой же, как у среды выполнения Python: процесс считывает аргументы из stdin и записывает результаты в stdout в заданном в конфигурации формате. См. раздел Форматы и заголовки фрагментов. В репозитории llm-token-udf есть примеры нативных UDF на Rust и Go со скриптами сборки.

Справочник по конфигурации

Перечисленные ниже настройки доступны в консоли и через API. Указаны значения по умолчанию для ClickHouse Cloud; в некоторых случаях они отличаются от значений по умолчанию в версии с открытым исходным кодом. Следующими настройками версии с открытым исходным кодом управляет ClickHouse Cloud, и изменить их для отдельной UDF нельзя: command_termination_timeout, lifetime, stderr_reaction, check_exit_code и execute_direct. По умолчанию pool_size в версии с открытым исходным кодом равен 16, а в Cloud — 3. Полный список настроек версии с открытым исходным кодом приведён в разделе Исполняемые пользовательские функции. Имена функций должны соответствовать шаблону ^[A-Za-z][A-Za-z0-9_]*$, быть уникальными в пределах организации и не совпадать с именами встроенных функций ClickHouse; зарезервированные имена API отклоняет. У каждого аргумента должно быть имя, соответствующее тому же шаблону; API и Terraform требуют его при любом формате. Архивы с символическими ссылками отклоняются.

Форматы и заголовки фрагментов

TabSeparated — формат по умолчанию и самый простой. При его использовании символы табуляции, переводы строк и обратные слеши необходимо экранировать с обеих сторон. Формат RowBinary безопасен для двоичных данных и избавляет ClickHouse от форматирования и разбора текста; для длинных строк лучше использовать именно его. При включённом параметре Send chunk header ClickHouse перед каждым фрагментом записывает количество строк в виде десятичного числа и символ \n, а затем сами строки в заданном формате. Процесс должен прочитать ровно столько строк, записать столько же результатов и один раз сбросить вывод. Без этого параметра процесс, который читает строчно-ориентированный формат, например TabSeparated, RowBinary или JSONEachRow, и буферизует вывод, не может определить, где заканчивается блок, и запрос зависает до истечения тайм-аута чтения. Включайте этот параметр для любой такой UDF, которая не сбрасывает вывод после каждой строки. Формат Native уже содержит границы блоков, поэтому UDF с форматом Native может прочитать один блок, записать один блок и сбросить вывод без заголовка.

Сетевой доступ

Сетевой доступ поддерживается только для среды выполнения Python; для нативных UDF включить его нельзя. Нативные UDF Python, созданные в консоли, собираются с включённым сетевым доступом, и отдельной настройки для него в консоли нет. В API параметр sandboxType позволяет выбрать между netenable, который разрешает исходящие подключения, и basic, который их запрещает; в Terraform-провайдере тот же выбор доступен через параметр sandbox_type. Сетевой доступ разрешает исходящие подключения к конечным точкам в публичном интернете. Подключения к частным сетям, link-local-адресам и внутренним адресам блокируются. У песочницы нет облачной идентичности: нет роли инстанса, нет конечной точки метаданных и нет наследуемых переменных окружения, поэтому UDF располагает только теми учётными данными, которые вы ей передадите. Сетевой доступ применяется при сборке версии UDF, поэтому, если существующая версия была создана без него, нужно создать новую версию. Хранилища секретов для UDF пока нет. Включите файл конфигурации в zip-архив и считывайте его по пути относительно __file__; никогда не указывайте ключи в SQL.

Лимит памяти

Лимит памяти задаёт максимальный объём памяти (в МиБ), доступный каждому процессу песочницы. Если параметр не задан, действует значение песочницы по умолчанию — 4 ГиБ. Ограничение распространяется на виртуальное адресное пространство, а не на резидентную память, поэтому средам выполнения, которые резервируют большие диапазоны адресов, нужен запас, намного превышающий их фактическое потребление. Так, Go при запуске резервирует заметно больше 1 ГиБ адресного пространства. Если процесс превышает ограничение, выделение памяти завершается неудачей внутри самого процесса: UDF Python завершается с трассировкой MemoryError, а запрос — ошибкой, в сообщении которой приводится содержимое stderr.

Детерминированные функции и кэш запросов

По умолчанию UDF считается недетерминированной. Запрос, вызывающий такую функцию при use_query_cache = 1, завершается ошибкой QUERY_CACHE_USED_WITH_NONDETERMINISTIC_FUNCTIONS, если не изменено значение настройки query_cache_nondeterministic_function_handling. Включайте детерминированность только в том случае, если функция при одних и тех же аргументах всегда возвращает один и тот же результат. Тогда кэш запросов сможет сохранять и отдавать результаты запросов, которые её вызывают. Другие недетерминированные функции в том же запросе, например now, today или rand, по-прежнему препятствуют кэшированию при обработке по умолчанию (throw). При query_cache_nondeterministic_function_handling = 'save' такие запросы всё равно кэшируются, а при 'ignore' выполняются без кэширования.

Перезапуск сервиса

Первое подключение приводит к перезапуску сервисаПри подключении к сервису первой UDF в его поды добавляется вспомогательный контейнер и выполняется поочередный перезапуск. Подключение последующих UDF перезапуска не вызывает. При удалении последней UDF из сервиса снова выполняется поочередный перезапуск. Учитывайте это при планировании первого подключения.

Мониторинг

Начиная с версии 26.6, ClickHouse фиксирует использование ресурсов исполняемыми UDF в восьми счётчиках, имена которых начинаются с префикса ExecutableUserDefinedFunction. На уровне отдельного запроса они хранятся как записи в столбце типа Map ProfileEvents таблицы system.query_log. На уровне всего сервера они доступны как строки таблицы system.events (накопительные значения с момента запуска сервера), а также как столбцы ProfileEvent_<name> таблицы system.metric_log (значения, периодически собираемые во времени). Суффиксы счётчиков: Таблица system.asynchronous_metrics содержит метрики ExecutableUserDefinedFunctionProcesses и ExecutableUserDefinedFunctionMemoryResidentBytes. Вторая из них — это сумма VmRSS по всем работающим процессам UDF. Она даёт лишь верхнюю оценку, поскольку общие страницы памяти учитываются в каждом процессе отдельно.
system.query_log.memory_usage учитывает только процесс сервера ClickHouse и не включает память процессов UDF.

Рекомендации по производительности

Используйте executable_pool. В режиме executable для каждого блока данных запускается новый процесс, поэтому накладные расходы на запуск приходятся на каждый блок. У каждого вызова есть фиксированная стоимость. Если значение ExecutableUserDefinedFunctionInvocations очень велико по сравнению с количеством обработанных строк, увеличьте preferred_block_size_bytes для этого запроса, чтобы каждый блок содержал больше строк. Один запрос одновременно использует не более max_threads процессов пула, поэтому пул размером больше max_threads не ускорит этот запрос. Начните с небольшого пула и увеличивайте его размер, если значение ExecutableUserDefinedFunctionPoolWaitMicroseconds становится заметным. Каждая строка пересекает границу процесса. Пользовательские функции (UDF) предназначены для логики, которую невозможно выразить на SQL, а не для тривиальной построчной обработки, с которой справится встроенная функция или SQL UDF.

Устранение неполадок

Если UDF завершается сбоем, запрос завершается с ошибкой с кодом 754 (UDF_EXECUTION_FAILED). Содержимое stderr дочернего процесса включается в сообщение об ошибке, поэтому выводите диагностическую информацию туда и завершайте процесс с ненулевым кодом возврата:
Типичные ситуации:
  • Запрос зависает, а затем завершается с ошибкой по тайм-ауту: процесс буферизовал вывод в строково-ориентированном формате без Send chunk header. Включите этот параметр и сбрасывайте вывод после каждого фрагмента, а при использовании формата Native — после каждого блока.
  • Код возврата 1 и трассировка стека MemoryError (Python): превышен лимит памяти. Увеличьте значение лимита памяти или сократите потребление памяти каждым процессом.
  • Периодические сбои при типе executable под высокой нагрузкой: переключитесь на executable_pool.
  • Статус сборки — error: в корне zip-архива отсутствует main.py (Python) или amd64/main и arm64/main (Native), либо архив содержит символические ссылки.
  • Не удаётся создать функцию: её имя совпадает с именем встроенной функции.

Управление пользовательскими функциями (UDF) через Cloud API

Операции жизненного цикла, доступные в консоли, также можно выполнять программно через ClickHouse Cloud API. Конечные точки UDF позволяют автоматизировать загрузку исходных архивов, создание функций и версий, их подключение к сервисам и отключение от них, а также удаление версий и функций. В API нет операции перезагрузки: после подключения новой версии UDF типа executable_pool к сервису выполните в этом сервисе SQL-команду SYSTEM RELOAD FUNCTION ON CLUSTER default <name>, чтобы процессы пула начали использовать новую версию.
Эти конечные точки находятся на стадии бета-тестирования, и контракт API может измениться.
Типичный процесс создания и развертывания UDF через API:
  1. Создайте URL для загрузки, чтобы получить предварительно подписанный URL для загрузки application/zip, а затем загрузите по нему ZIP-архив. Каждый ID загрузки можно использовать только для одной попытки создания UDF или её версии; при повторной попытке запросите новый URL для загрузки.
  2. Создайте UDF из загруженного архива, указав имя функции, среду выполнения, аргументы и возвращаемый тип.
  3. Подключите UDF к сервису. Если версия не указана, подключается последняя готовая версия. Сервис должен быть запущен; бездействующие сервисы можно предварительно активировать.
Поля запроса, определяющие описанные выше возможности: Полный список конечных точек: См. справочник API UDF со схемами запросов и ответов.

Управление пользовательскими функциями (UDF) с помощью Terraform

Официальный Terraform-провайдер ClickHouse включает два ресурса для управления пользовательскими функциями (UDF) в рамках подхода «инфраструктура как код»:
  • clickhouse_udf управляет самой функцией. Он принимает ZIP-архив с исходным кодом функции и публикует новую версию при изменении хеша архива, дожидаясь завершения сборки.
  • clickhouse_udf_attachment подключает версию UDF к сервису. К сервису можно одновременно подключить не более одной версии функции. Можно закрепить конкретный номер версии или сослаться на clickhouse_udf.<name>.version, чтобы автоматически обновлять сервисы до последней версии.
Эти ресурсы доступны начиная с версии провайдера 3.24.0. Они имеют статус бета, и их поведение может измениться в будущих версиях провайдера.
Например, чтобы развернуть с помощью Terraform UDF isBusinessHours из предыдущего примера:
Помимо обязательных атрибутов, показанных выше, ресурс clickhouse_udf принимает format, pool_size, max_command_execution_time, command_read_timeout, command_write_timeout, send_chunk_header, sandbox_type, sandbox_version, return_name и fail_on_build_error; полное описание схемы см. в документации ресурса. Чтобы протестировать новую версию на сервисе для разработки до того, как она попадёт в продакшн, настройте одно подключение на отслеживание последней версии, а для другого закрепите версию с помощью переменной. Подключение на сервисе для разработки автоматически переходит на каждую новую сборку, а подключение в продакшн обновляется только при изменении var.prod_udf_version:
Провайдер пока не поддерживает параметр deterministic и ограничение памяти. Оба параметра задаются при создании версии, поэтому у версий, опубликованных провайдером, их быть не может. Чтобы использовать эти параметры, создавайте такие версии через консоль или API.
Подключение возможно только для версий в состоянии готовности и может занять несколько минут; бездействующие сервисы запускаются автоматически. Пулы провайдер также не перезагружает: для UDF типа executable_pool после смены версии в подключении выполните на сервисе SYSTEM RELOAD FUNCTION ON CLUSTER default <name>. При удалении ресурса clickhouse_udf удаляются все версии функции, и она отключается от всех сервисов.