alitrack

在 DuckDB 里跑 JavaScript:duckdb-luajit 内嵌 QuickJS

duckdb-luajit 是 DuckDB 的 LuaJIT UDF 扩展:SQL 里写 Lua 函数, ffi 直接调 C 库。
这篇文章实测它新增的一条路: SQL 里执行 JavaScript ——不用装 Node、不用起子进程,QuickJS 引擎直接内嵌进 DuckDB 进程。

● ● ●

为什么要在 SQL 里跑 JS

SQL 里总有 Lua/正则表达式够不着的逻辑。最典型的是 Unicode 属性正则 : \p{L} 匹配任意语言的字母(含中文、日文假名),Lua 的 pattern 做不到——Lua 正则没有 Unicode 属性转义,遇到「提取一段文本里的所有中英文单词」这种需求只能写一堆字符区间。

还有现成的 JS 生态:day.js 日期处理、已有的前端/后端 JS 函数,想在 SQL 里复用。

● ● ●

三条路,实测对比

方案 每次调用开销 说明
子进程 node -e ~50–100 ms 进程启动 + 数据序列化
DuckDB 官方 python 扩展 重 要装 Python 运行时
FFI 内嵌 QuickJS 61.7 μs 引擎在进程内,无序列化
子进程慢 800–1600 倍。内嵌引擎的关键是 QuickJS——纯 C、零依赖、单文件源码,编译出的 libquickjs.so 只有 1.1 MB ,正好能被 duckdb-luajit 的 ffi.load 直接加载。

● ● ●

实现:三层,加起来不到 30 行

duckdb-luajit 内嵌 QuickJS 三层架构

duckdb-luajit 内嵌 QuickJS 三层架构

第一层:QuickJS 源码编译 (1 条 make + 1 条 gcc,产出 1.1 MB 的 .so) 第二层:20 行 C 包装桥 。QuickJS 的 JSValue 是 16 字节结构体,直接跨 FFI 边界传递有 ABI 坑,所以用一个薄 C 桥消化所有细节,只暴露两个字符串函数:
const char *luaqjs_eval(const char *src);   /* 执行 JS, 返回结果字符串 */
void luaqjs_free(const char *s); /* 释放结果 */
第三层:SQL 里 3 行注册一个 UDF ,之后任何查询都能调:
LOAD 'luajit';
SELECT * FROM luajit_module(mode := 'compile', sql_name := 'qjs',
source := 'return function(x) ... ffi.load("luaqjs_bridge.so") ... end');
SELECT luajit_s('qjs', 'x'); -- 7

● ● ●

实测用例

① 算术 : luajit_s('qjs', 'x') → 7 (内部执行 1+2*3 ) ② Unicode 属性正则(Lua 做不到的) :
-- JS 源码: "Café 中文 123 テスト".match(/\p{L}+/gu).join(",")
SELECT luajit_s('jsre', 'x');
-- → Café,中文,テスト
一段混合 Café 中文 123 テスト , \p{L} 一行把英、中、日三种字母全部提出,数字被正确跳过。这个能力 Lua pattern 给不了。 ③ JSON 结构化输出 : JSON.stringify({name:"duckdb-luajit", version:0.3, tags:["lua","js"]}) → 完整 JSON 字符串,可直接配 DuckDB 的 from_json 解析成列。

● ● ●

性能:57.9 μs / 次

10 万次调用(每次独立创建 QuickJS runtime、执行、释放), 10 万次全部成功,总耗时 5.8 秒 ,单次 57.9 μs 。对比子进程 node 的 50–100 ms,快了约 1000 倍。 同环境实测 DuckDB 社区扩展 quickjs(QuickJS-NG 引擎)做同样的 10 万次调用:单次 142–153 μs 。差异来自实现路径:社区扩展每行多做全局对象创建、console 注入、严格模式编译、DuckDB↔JS 值转换与 JSON 序列化(它的 quickjs_eval 支持参数绑定,功能更全);本文方案是字符串进出的最简桥。两者都每次新建 runtime,复用 runtime 是共同的优化空间。 (踩坑提醒:性能测试别用常量参数——DuckDB 优化器会把 WHERE udf('a','b')=... 折叠掉,测试要传列参数 udf('qjs', CAST(range AS VARCHAR)) ,否则测的是优化器的速度不是你的 UDF。)

● ● ●

边界与下一步

  • QuickJS 是 ES2020+ 子集引擎:Promise、async、 \p{} 正则、TypedArray 都支持; 没有 DOM、Node API。
  • 当前每次调用新建 runtime(隔离最干净),复用 runtime 可再快一个数量级——这是给高频场景的优化方向。
  • 下一篇预告 :SQL 里执行 Wasm——QuickJS 只解决 JS,Wasm 解决「任意语言」(Rust/Go/C 编译成 wasm32-wasi 都能在 SQL 里跑),且自带沙箱隔离。
实测环境:WSL2 Ubuntu,duckdb v1.5.5,duckdb-luajit v0.30,QuickJS 2026-06-04 源码编译。所有数字为本地实测,非估算。

● ● ●

参考来源

  1. 01 QuickJS 官方仓库(bellard/quickjs):https://github.com/bellard/quickjs
  2. 02 DuckDB 官方文档:https://duckdb.org/docs/
  3. 03 DuckDB 社区扩展 quickjs(本文对比对象):https://duckdb.org/community_extensions/extensions/quickjs
  4. 04 duckdb-luajit 扩展(作者本人项目,WSL2 本地源码路径 /mnt/d/wsl2/luajit)