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

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

ClickHouse поддерживает создание пользовательских функций (UDF) на WebAssembly. Это позволяет выполнять собственную логику, написанную на Rust, C, C++ и других языках, скомпилировав её в модули WebAssembly.

Обзор

Модуль WebAssembly — это скомпилированный бинарный файл, содержащий одну или несколько функций, которые можно вызывать из ClickHouse. Модуль можно рассматривать как библиотеку или разделяемый объект, который загружается один раз и затем многократно используется. Модуль WebAssembly с пользовательскими функциями (UDF) можно написать на любом языке, который компилируется в WebAssembly, например на Rust, C или C++. Код, скомпилированный в WebAssembly («гостевой» код) и выполняемый ClickHouse («хостом»), работает в изолированной песочнице и имеет доступ только к выделенной области памяти. Гостевой код экспортирует функции, которые может вызывать ClickHouse. К ним относятся как функции, реализующие вашу пользовательскую логику (используемую для определения UDF), так и вспомогательные функции, необходимые для управления памятью и обмена данными между ClickHouse и кодом WebAssembly. Ваш код должен быть скомпилирован в «автономный» WebAssembly (то есть wasm32-unknown-unknown) без каких-либо зависимостей от операционной системы или стандартной библиотеки. Кроме того, поддерживается только стандартная 32-битная целевая платформа WebAssembly (без расширения wasm64). Модуль должен соответствовать одному из поддерживаемых протоколов взаимодействия (ABI) для работы с ClickHouse. После компиляции бинарный код модуля загружается в ClickHouse путём вставки в таблицу system.webassembly_modules. После этого вы можете создавать UDF, ссылающиеся на функции, экспортируемые модулем, с помощью оператора CREATE FUNCTION ... LANGUAGE WASM.

Предварительные требования

Включите поддержку WebAssembly в конфигурации ClickHouse:
<clickhouse>
    <allow_experimental_webassembly_udf>true</allow_experimental_webassembly_udf>
    <webassembly_udf_engine>wasmtime</webassembly_udf_engine>
</clickhouse>
Доступные реализации движка:
  • wasmtime (по умолчанию; рекомендуется) — использует WasmTime
  • wasmedge — использует WasmEdge

Быстрый старт

Этот пример демонстрирует полный процесс создания WebAssembly UDF на примере реализации вычисления гипотезы Коллатца. Мы напишем код в текстовом формате WebAssembly (WAT) — удобочитаемом представлении WebAssembly, поэтому на этом этапе не потребуется какой-либо язык программирования. ClickHouse требует, чтобы модуль был в бинарном формате, поэтому мы воспользуемся транспилятором, чтобы преобразовать WAT в WASM. Для этого преобразования можно использовать wat2wasm из WebAssembly Binary Toolkit (WABT) или команду parse из wasm-tools.
cat << 'EOF' | wasm-tools parse | clickhouse client -q "INSERT INTO system.webassembly_modules (name, code) SELECT 'collatz', code FROM input('code String') FORMAT RawBlob"
(module
  (func $next (param $n i32) (result i32)
    local.get $n i32.const 1 i32.and
    (if (result i32)
      (then local.get $n i32.const 3 i32.mul i32.const 1 i32.add)
      (else local.get $n i32.const 2 i32.div_u)))
  (func $steps (export "steps") (param $n i32) (result i32)
    (local $count i32)
    local.get $n i32.const 1 i32.lt_u
    (if (then i32.const 0 return))
    (block $done (loop $loop
      local.get $n i32.const 1 i32.eq br_if $done
      local.get $n call $next local.set $n
      local.get $count i32.const 1 i32.add local.set $count
      br $loop))
    local.get $count)
)
EOF
В приведённом выше фрагменте мы напрямую передаём бинарный код WASM в клиент ClickHouse с помощью FORMAT RawBlob, чтобы выполнить вставку в таблицу system.webassembly_modules. Затем мы определяем UDF, которая ссылается на функцию steps, экспортируемую этим модулем:
CREATE FUNCTION collatz_steps LANGUAGE WASM ARGUMENTS (n UInt32) RETURNS UInt32 FROM 'collatz' :: 'steps';
Обратите внимание, что после :: мы указываем имя функции из модуля, поскольку оно отличается от имени UDF. Теперь мы можем использовать функцию collatz_steps в запросах:
SELECT groupArray(collatz_steps(number :: UInt32))
FROM numbers(1, 100)
FORMAT TSV
Столбец number явно приводится к типу UInt32, поскольку функции WebAssembly ожидают точного соответствия типов сигнатуре, указанной в операторе CREATE FUNCTION. В результате мы получили последовательность шагов Коллатца для чисел от 1 до 100, соответствующую последовательности A006577 из OEIS.
[0,1,7,2,5,8,16,3,19,6,14,9,9,17,17,4,12,20,20,7,7,15,15,10,23,10,111,18,18,18,106,5,26,13,13,21,21,21,34,8,109,8,29,16,16,16,104,11,24,24,24,11,11,112,112,19,32,19,32,19,19,107,107,6,27,27,27,14,14,14,102,22,115,22,14,22,22,35,35,9,22,110,110,9,9,30,30,17,30,17,92,17,17,105,105,12,118,25,25,25]

Управление модулями WASM через системную таблицу

Модули WebAssembly хранятся в таблице system.webassembly_modules со следующей структурой:
  • Столбцы
    • name String — Имя модуля. Не пустое, только буквенно-цифровые символы и знак подчёркивания.
    • code String — Необработанный бинарный код WASM. Только для записи; при чтении возвращается пустая строка.
    • hash UInt256 — SHA256 бинарного файла модуля (ноль, если он есть на диске, но ещё не загружен).
Управление модулями выполняется с помощью стандартных SQL-операций над этой таблицей:

Добавление модуля

INSERT INTO system.webassembly_modules (name, code)
SELECT 'my_module', base64Decode('AGFzbQEAAAA...');
При необходимости укажите хеш для проверки целостности:
INSERT INTO system.webassembly_modules (name, code, hash)
SELECT 'my_module', base64Decode('...'), reinterpretAsUInt256(unhex('369f...c57d'));
Если указанный хеш не совпадает с вычисленным SHA256 кода модуля, вставка завершится ошибкой. Это может быть полезно при загрузке модулей из внешних источников, таких как S3 или HTTP.

Распределение модуля по cluster

system.webassembly_modules — это таблица уровня instance: INSERT выполняется только на той реплике, которая обрабатывает connection. Для оператора INSERT нет формы ON CLUSTER, поэтому последующий CREATE FUNCTION ... ON CLUSTER завершится ошибкой на репликах, где этого модуля нет:
Code: 674. DB::Exception: WebAssembly module 'collatz' not found:
while adding user defined function `collatz_steps`. (RESOURCE_NOT_FOUND)
Чтобы выполнить insert на всех узлах, записывайте в табличную функцию cluster, а не в локальную таблицу system.webassembly_modules:
cat collatz.wasm | clickhouse client -q "
  INSERT INTO FUNCTION cluster('default', 'system', 'webassembly_modules') (name, code)
  SELECT 'collatz', code FROM input('code String') FORMAT RawBlob"
Этот подход опирается на то, что базовый путь распределённой записи проходит через все реплики в каждом сегменте, а это происходит только если для кластера задано internal_replication=false. При internal_replication=true (значение по умолчанию для кластеров, использующих ReplicatedMergeTree для собственной репликации) вставка отправляется только на одну работоспособную реплику в каждом сегменте, а system.webassembly_modules по этому пути не реплицируется — поэтому на части реплик модуль по-прежнему будет отсутствовать. В такой конфигурации нужно выполнять вставку в каждую реплику отдельно, например перебирая system.clusters и записывая через remote(...) для каждого хоста, либо копируя бинарный файл в user_scripts/wasm/ на каждом хосте.Проверить значение internal_replication для кластера можно с помощью SELECT cluster, shard_num, internal_replication FROM system.clusters.
После такой веерной вставки модуль будет присутствовать на всех репликах, и CREATE FUNCTION ... ON CLUSTER выполнится успешно:
CREATE FUNCTION collatz_steps ON CLUSTER 'default'
LANGUAGE WASM FROM 'collatz' :: 'steps'
ARGUMENTS (n UInt32) RETURNS UInt32;
Вы можете проверить, что модуль загружен на всех репликах, с помощью clusterAllReplicas:
SELECT hostName(), name FROM clusterAllReplicas('default', system.webassembly_modules) WHERE name = 'collatz';
Вставки в system.webassembly_modules идемпотентны для одной и той же пары (name, hash), поэтому повторный запуск распределённой вставки безопасен и является разумным способом восстановить состояние после замены реплики. Обратите внимание: вновь добавленные серверы не получают существующие модули задним числом — необходимо повторно выполнить вставку в обновлённый кластер или поместить бинарный файл в каталог user_scripts/wasm/ на новом хосте.

Просмотр списка модулей

SELECT name, lower(hex(reinterpretAsFixedString(hash))) AS sha256 FROM system.webassembly_modules

   ┌─name────┬─sha256───────────────────────────────────────────────────────────┐
1. │ collatz │ a084a10b7b5cb07db198bc93bf1f3c1f8cb8ef279df7a4f6b66b1cdd55d79c48 │
   └─────────┴──────────────────────────────────────────────────────────────────┘

Удаление модуля

Удаление выполняется оператором DELETE FROM system.webassembly_modules WHERE name = '...'. Предикат должен быть либо name = 'literal' для точного совпадения, либо name LIKE 'pattern' для удаления всех модулей, имя которых соответствует шаблону; другие формы не допускаются.
DELETE FROM system.webassembly_modules WHERE name = 'collatz';

-- Массовое удаление всех модулей, имя которых начинается с `tmp_` (символ подчёркивания экранируется как `\_`):
DELETE FROM system.webassembly_modules WHERE name LIKE 'tmp\_%';
Если какая-либо из существующих пользовательских функций (UDF) ссылается на один из найденных модулей, удалить его не удастся, поэтому сначала нужно удалить эти пользовательские функции (UDF).

Создание WebAssembly UDF

Синтаксис:
CREATE [OR REPLACE] FUNCTION function_name
LANGUAGE WASM
FROM 'module_name' [:: 'source_function_name']
ARGUMENTS ( [name type[, ...]] | [type[, ...]] )
RETURNS return_type
[ABI ROW_DIRECT | ABI BUFFERED_V1 | ABI ASSEMBLYSCRIPT]
[DETERMINISTIC]
[SHA256_HASH 'hex']
[SETTINGS key = value[, ...]];
Параметры:
  • function_name: Имя функции в ClickHouse. Может отличаться от имени экспортируемой функции в модуле.
  • FROM 'module_name' :: 'source_function_name': Имя загруженного WASM-модуля и имя функции в WASM-модуле, которую следует использовать (по умолчанию — function_name)
  • ARGUMENTS: Список имён и типов аргументов (имена необязательны и используются для форматов сериализации, поддерживающих именованные поля)
  • ABI: Версия Application Binary Interface
    • ROW_DIRECT: Прямое сопоставление типов, построчная обработка
    • BUFFERED_V1: Обработка блоками с сериализацией
    • ASSEMBLYSCRIPT: Построчная обработка для модулей, созданных компилятором AssemblyScript. Числовые типы сопоставляются с примитивами AssemblyScript; ClickHouse String сопоставляется с типом AssemblyScript string.
  • DETERMINISTIC: Объявляет функцию детерминированной — она всегда возвращает один и тот же результат для одних и тех же входных данных. Если указан этот параметр, ClickHouse может выполнить константную свёртку вызовов, в которых все аргументы являются константами: функция вычисляется один раз на этапе анализа запроса, а результат затем повторно используется для каждой строки.
  • SHA256_HASH: Ожидаемый хеш модуля для проверки (заполняется автоматически, если опущен); может использоваться, чтобы гарантировать загрузку правильного WASM-модуля на разных репликах.
  • SETTINGS: Настройки функции
    • serialization_format String — Формат сериализации, если он требуется для ABI. Поддерживаемые значения: MsgPack, JSONEachRow, CSV, TSV, TSVRaw, RowBinary и Buffers. По умолчанию: MsgPack. Форматы с блочной обработкой, такие как Buffers, должны возвращать один столбец, тип которого соответствует объявленной сигнатуре функции.
    • webassembly_udf_enable_fuel Bool — Включает ограничение по fuel для функции. По умолчанию: true. Если указано false, настройка уровня запроса webassembly_udf_max_fuel для этой функции игнорируется. Отключение ограничений fuel может повысить производительность при использовании движка wasmtime. Однако для недоверенного или содержащего ошибки гостевого кода это может увеличить риск неконтролируемого выполнения.

Версии ABI

Для взаимодействия с ClickHouse модули WebAssembly должны соответствовать одному из поддерживаемых ABI (Application Binary Interface).
  • ROW_DIRECT: Прямое отображение типов (только примитивные типы Int32, UInt32, Int64, UInt64, Float32, Float64)
  • BUFFERED_V1: Сложные типы с сериализацией
  • ASSEMBLYSCRIPT: Построчное взаимодействие с модулями AssemblyScript; поддерживает числовые типы и String.

ABI ROW_DIRECT

Напрямую вызывает экспортируемую функцию WASM для каждой строки.
  • Аргументы и возвращаемые значения должны иметь числовые типы Int32/UInt32/Int64/UInt64/Float32/Float64/Int128/UInt128.
  • Строки в этом ABI не поддерживаются.
  • Сигнатуры должны соответствовать экспорту WASM (i32/i64/f32/f64/v128).
  • Модуль не обязан экспортировать какие-либо вспомогательные функции.
Например, функция со следующей сигнатурой:
(func (param i32 i64 f32) (result f64) ...)
Можно создать так:
CREATE FUNCTION my_func ARGUMENTS (Int32, UInt64, Float32) RETURNS Float64 ...
WebAssembly не различает знаковые и беззнаковые аргументы, а использует разные инструкции для интерпретации значений. Поэтому размер аргумента должен точно соответствовать, а знаковость определяется операциями внутри функции.

ABI BUFFERED_V1

Этот ABI является экспериментальным и может измениться в будущих версиях.
Обрабатывает блоки целиком, используя (де)сериализацию через память WASM. Поддерживает любые типы аргументов и возвращаемых значений. Сериализованные данные копируются в память WASM и передаются в функцию UDF как указатель на буфер (который состоит из указателя на данные и их размера) вместе с количеством входных строк. Таким образом, user-defined function в WASM всегда принимает два аргумента i32 и возвращает одно значение i32. Гостевой код обрабатывает данные и возвращает указатель на буфер результата с сериализованными результирующими данными. Гостевой код должен предоставлять две функции для создания и освобождения этих буферов.
(module
  ;; Выделить новый буфер указанного размера
  ;; Возвращает: дескриптор структуры Buffer (не прямой указатель на данные!) с указателем на данные и их размером
  (func (export "clickhouse_create_buffer")
    (param $size i32)    ;; Размер выделяемых данных
    (result i32))        ;; Возвращает дескриптор буфера с достаточным пространством

  ;; Освободить буфер по его дескриптору
  (func (export "clickhouse_destroy_buffer")
    (param $handle i32)  ;; Дескриптор буфера для освобождения
    (result))            ;; Нет возвращаемого значения

    ;; Пользовательская функция
    (func (export "user_defined_function1")
      (param $input_buffer_handle i32)  ;; Дескриптор входного буфера
      (param $n i32)                    ;; Количество строк во входных данных
      (result i32))                     ;; Возвращает дескриптор выходного буфера
)
Пример определений на C:
typedef struct {
    uint8_t * data;
    uint32_t size;
} ClickhouseBuffer;

ClickhouseBuffer * clickhouse_create_buffer(uint32_t size) { /* ... */ }

void clickhouse_destroy_buffer(ClickhouseBuffer * data) { /* ... */ }

/// Примеры пользовательских функций
ClickhouseBuffer * user_defined_function1(ClickhouseBuffer * span, uint32_t n) { /* ... */ }
ClickhouseBuffer * user_defined_function2(ClickhouseBuffer * span, uint32_t n) { /* ... */ }

ABI ASSEMBLYSCRIPT

Предназначен для модулей, созданных компилятором AssemblyScript. Для каждой строки выполняется один вызов экспортируемой функции, при этом значения ClickHouse преобразуются в примитивы AssemblyScript и строковые объекты. Поддерживаемые типы:
  • Числовые: Int8/UInt8, Int16/UInt16 (на границе расширяются до i32), Int32/UInt32, Int64/UInt64, Float32, Float64
  • String — сопоставляется со строковым типом AssemblyScript string (UTF-16 в памяти WASM). ClickHouse автоматически выполняет преобразование UTF-8 ↔ UTF-16.
  • Пользовательские классы AssemblyScript не поддерживаются в качестве типов аргументов или возвращаемых значений — их идентификаторы классов среды выполнения нестабильны между компиляциями (см. AssemblyScript#2982).
Требования к модулю: Модуль должен быть скомпилирован с управляемой средой выполнения AssemblyScript, чтобы экспортировались __new, __pin и __unpin. Стандартный механизм обработки входящих и выходных строк рассчитан на их наличие. Рекомендуемая команда:
asc src.ts --runtime incremental --exportRuntime -o src.wasm
AssemblyScript также импортирует env.abort для ловушек среды выполнения (нехватка памяти, проверка границ и т. д.). ClickHouse автоматически предоставляет этот импорт: при срабатывании abort активный запрос завершается с исключением WASM_ERROR, которое включает декодированное сообщение AssemblyScript и местоположение в исходном коде. Пример:
// src.ts
export function add(a: u32, b: u32): u32 {
  return a + b;
}

export function greet(name: string): string {
  return "Hello, " + name + "!";
}
После компиляции с помощью asc и загрузки полученного файла .wasm в system.webassembly_modules объявите пользовательские функции (UDF) следующим образом:
CREATE FUNCTION as_add
    LANGUAGE WASM ABI ASSEMBLYSCRIPT
    FROM 'as_example' :: 'add'
    ARGUMENTS (a UInt32, b UInt32) RETURNS UInt32;

CREATE FUNCTION as_greet
    LANGUAGE WASM ABI ASSEMBLYSCRIPT
    FROM 'as_example' :: 'greet'
    ARGUMENTS (name String) RETURNS String;

Примечание о разработке пользовательских функций (UDF) на Rust

Для программ на Rust мы предоставляем вспомогательный крейт clickhouse-wasm-udf, который упрощает разработку пользовательских функций (UDF) WebAssembly для ClickHouse. Крейт предоставляет функции для управления памятью, поэтому вам не нужно вручную реализовывать clickhouse_create_buffer и clickhouse_destroy_buffer — достаточно добавить этот крейт в зависимости. Также в нём есть макросы #[clickhouse_wasm_udf], которые оборачивают обычные функции Rust в требуемый ABI-формат. С этим крейтом вы можете писать пользовательские функции (UDF) так:

use clickhouse_wasm_udf_bindgen::clickhouse_udf;

#[clickhouse_udf]
pub fn some_udf(data: String) -> HashMap<String, String> {
    // Ваша реализация здесь
}

Макросы сгенерируют функцию-обёртку, которая принимает и возвращает структуры буфера и автоматически выполняет сериализацию и десериализацию с помощью serde.

API хоста, доступный модулям

Модули могут импортировать и использовать следующие функции хоста:
  • clickhouse_server_version() -> i64 — возвращает версию сервера ClickHouse в виде целого числа (например, 25011001 для v25.11.1.1).
  • clickhouse_throw(ptr: i32, size: i32) — вызывает ошибку с указанным сообщением. Принимает указатель на область памяти, содержащую строку сообщения об ошибке, и размер этой строки.
  • clickhouse_log(ptr: i32, size: i32) — записывает сообщение в текстовый журнал сервера ClickHouse.
  • clickhouse_random(ptr: i32, size: i32) — заполняет память случайными байтами.
  • env.abort(message: i32, fileName: i32, line: i32, column: i32) — предоставляется для модулей, совместимых с AssemblyScript. Его вызов (или срабатывание ловушки среды выполнения AssemblyScript, которая его вызывает) завершает UDF с исключением WASM_ERROR, содержащим декодированное сообщение и расположение в исходном коде. На модули, которые не импортируют env.abort, это не влияет.

Настройки

Следующие настройки на уровне запроса управляют выполнением WebAssembly UDF:
  • webassembly_udf_max_fuel — Лимит fuel для каждого запуска экземпляра WebAssembly UDF. Каждая инструкция WebAssembly расходует некоторое количество fuel. Значение масштабируется в 1024 раза перед передачей в runtime, поэтому webassembly_udf_max_fuel = 1 соответствует примерно 1024 единицам fuel. Установите 0, чтобы убрать конечное ограничение. Применяется только к функциям, для которых настройка уровня функции webassembly_udf_enable_fuel имеет значение true, что используется по умолчанию.
  • webassembly_udf_max_memory — Лимит памяти в байтах для каждого экземпляра WebAssembly UDF.
  • webassembly_udf_max_input_block_size — Максимальное количество строк, передаваемых в WebAssembly UDF в одном блоке. Установите 0, чтобы обрабатывать все строки сразу.
  • webassembly_udf_max_instances — Максимальное количество экземпляров WebAssembly UDF, которые могут выполняться параллельно для одной функции.
Пример использования:
SET webassembly_udf_max_fuel = 200000;
SELECT my_wasm_udf(column) FROM table;

См. также

Последнее изменение 25 июня 2026 г.