跳转到主要内容
ClickHouse 支持创建以 WebAssembly 编写的用户自定义函数 (UDF) 。这样,你就可以将 Rust、C、C++ 等语言编写的自定义逻辑编译为 WebAssembly 模块并执行。

概述

WebAssembly 模块是已编译的二进制文件,其中包含一个或多个可由 ClickHouse 调用的函数。 可以将模块看作一个只需加载一次、却能重复多次使用的库或共享对象。 包含 UDF 的 WebAssembly 模块可以使用任何能编译为 WebAssembly 的语言编写,例如 Rust、C 或 C++。 编译为 WebAssembly 的代码 (“guest”代码) 和执行它的 ClickHouse (“host”) 运行在沙箱环境中,只能访问专用的内存空间。 guest 代码会导出 ClickHouse 可调用的函数——这些函数既包括实现自定义逻辑的函数 (用于定义 UDF) ,也包括 ClickHouse 与 WebAssembly 代码之间进行内存管理和数据交换所需的辅助函数。 你的代码应编译为“freestanding” WebAssembly (也称为 wasm32-unknown-unknown) ,不依赖任何操作系统或标准库。此外,仅支持默认的 32 位 WebAssembly 目标 (不支持 wasm64 扩展) 。 该模块必须遵循一种受支持的通信协议 (ABI) 才能与 ClickHouse 交互。 编译完成后,需要将模块的二进制代码插入 system.webassembly_modules 表中,以将其加载到 ClickHouse。 之后,你可以使用 CREATE FUNCTION ... LANGUAGE WASM 语句创建引用该模块导出函数的 UDF。

前置条件

在 ClickHouse 配置中启用 WebAssembly 支持:
<clickhouse>
    <allow_experimental_webassembly_udf>true</allow_experimental_webassembly_udf>
    <webassembly_udf_engine>wasmtime</webassembly_udf_engine>
</clickhouse>
可用的引擎实现:

快速入门

本示例通过实现 Collatz conjecture 计算器,演示创建 WebAssembly UDF 的完整流程。 我们将使用 WebAssembly Text 格式 (WAT) 编写代码。它是 WebAssembly 的一种人类可读表示形式,因此在这一阶段无需使用任何编程语言。 ClickHouse 要求模块采用二进制格式,因此我们将使用转换工具将 WAT 转换为 WASM。 要执行此转换,可以使用 WebAssembly Binary Toolkit (WABT) 中的 wat2wasm,或 wasm-tools 中的 parse 命令。
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
在上面的代码片段中,我们使用 FORMAT RawBlob 将二进制 WASM 代码直接通过管道传给 ClickHouse 客户端,并将其插入到 system.webassembly_modules 表中。 然后,我们定义一个引用该模块导出的 steps 函数的 UDF:
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 各个数字对应的 Collatz 步骤序列,这与序列 OEIS 中的 A006577 相对应。
[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 等外部来源加载模块时,这会很有用。

列出模块

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\_%';
如果现有 UDFs 中有任何一个引用了某个匹配模块,删除操作将失败,因此必须先删除这些 UDFs。

创建 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]
[DETERMINISTIC]
[SHA256_HASH 'hex']
[SETTINGS key = value[, ...]];
参数
  • function_name:ClickHouse 中的函数名称。可能与模块中导出的函数名不同。
  • FROM 'module_name' :: 'source_function_name':已加载的 WASM 模块名称,以及要使用的 WASM 模块中的函数名称 (默认为 function_name)
  • ARGUMENTS:参数名称和类型列表 (名称为可选项,用于支持命名字段的序列化格式)
  • ABI:应用程序二进制接口版本
    • ROW_DIRECT:直接类型映射,按行处理
    • BUFFERED_V1:基于块的处理,带序列化
  • DETERMINISTIC:将该函数声明为确定性的——对于相同输入始终返回相同输出。指定后,当所有参数都是常量时,ClickHouse 可能会对调用进行常量折叠:函数会在查询分析阶段求值一次,结果会复用于每一行。
  • SHA256_HASH:用于校验的预期模块哈希 (若省略则自动填充) ,可用于确保不同副本上加载的是正确的 WASM 模块。
  • SETTINGS:函数级设置
    • serialization_format String —— ABI 所需的序列化格式。默认值:MsgPack

ABI 版本

要与 ClickHouse 交互,WebAssembly 模块必须遵循受支持的 ABI (应用程序二进制接口) 之一。
  • ROW_DIRECT:直接类型映射 (仅支持基本类型 Int32UInt32Int64UInt64Float32Float64)
  • BUFFERED_V1:通过序列化处理复杂类型

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 仍处于 Experimental 阶段,未来版本中可能会发生变化。
通过 WASM 内存中的 (反) 序列化一次处理整个块。支持任意参数和返回类型。 序列化后的数据会被复制到 WASM 内存中,并将指向缓冲区的指针 (缓冲区由数据指针和数据大小组成) 以及输入中的行数一并传递给 UDF 函数。因此,WASM 侧的用户自定义函数始终接收两个 i32 参数,并返回单个 i32 值。 guest 侧代码处理数据后,会返回一个指向结果缓冲区的指针,其中包含序列化后的结果数据。 guest 侧代码必须提供两个函数,用于创建和销毁这些缓冲区。
(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) { /* ... */ }

关于用 Rust 开发 UDF 的说明

对于 Rust 程序,我们提供了一个辅助 crate clickhouse-wasm-udf,可简化为 ClickHouse 开发 WebAssembly UDF 的过程。该 crate 提供了内存管理功能,因此你无需手动实现 clickhouse_create_bufferclickhouse_destroy_buffer 函数,只需将该 crate 添加为依赖即可。此外,还提供了宏 #[clickhouse_wasm_udf],可将普通的 Rust 函数封装为所需的 ABI 格式。 借助这个 crate,你可以像这样编写 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 server 版本 (例如,v25.11.1.1 对应 25011001) 。
  • clickhouse_throw(ptr: i32, size: i32) — 使用给定的消息抛出错误。接受一个指向存放错误消息字符串的内存位置的指针,以及该字符串的大小。
  • clickhouse_log(ptr: i32, size: i32) — 将消息写入 ClickHouse server 的文本日志。
  • clickhouse_random(ptr: i32, size: i32) — 用随机字节填充内存。

设置

以下查询级设置用于控制 WebAssembly UDF 的执行:
  • webassembly_udf_max_fuel — 每个 WebAssembly UDF 实例每次执行的 fuel 上限。每条 WebAssembly 指令都会消耗一定数量的 fuel。设为 0 表示不限制。
  • 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;

另请参阅

最后修改于 2026年6月10日