alitrack

在 SQL 里直接写 Python——我做了一个 DuckDB 扩展

py_register('double_it', 'str(int(x)*2)', 1)
● ● ●

为什么做这个

DuckDB 最爽的一点是 LOAD 一个扩展就能获得新能力—— LOAD httpfs 读 S3, LOAD spatial 做 GIS。但你没法在 SQL 里注册一个 Python 函数:
-- SQLite 可以
SELECT python_function('hello');

-- PostgreSQL 可以
CREATE FUNCTION py_upper(text) RETURNS text LANGUAGE plpython3u AS $$ return args[0].upper() $$;

-- DuckDB?没有。
duckdb-python 就是填这个坑的——一个 DuckDB 社区扩展,把 CPython 嵌入 DuckDB 进程,让你在 SQL 里注册、调用 Python 函数。 ● ● ●

一行注册,直接调用

LOAD './build/release/extension/python/python.duckdb_extension';

SELECT py_register('double_it', 'str(int(x)*2)', 1);  -- 返回 'OK'
SELECT double_it('21');                                   -- 返回 '42' ✨
SELECT double_it('99');                                   -- 返回 '198'
SELECT py_call('double_it', '5');                         -- 返回 '10'(兼容方式)
注册后函数就是原生 DuckDB 标量函数,可以直接按名字调用——不需要 py_call() 包装。 函数名会做校验: double 、 select 等 SQL 保留字和类型名会被拒绝,数字开头、含连字符的名字也会报错,避免注册了但调不了。 ● ● ●

全部函数一览

函数 类型 作用
py_eval(expr, value) Scalar 逐行 eval Python 表达式, x 绑定到 value
py_map(expr, value) Scalar 批量列 eval,一次 Python 调用处理整个 chunk
py_register(name, expr, nargs) Scalar 注册命名函数, 直接可调用
py_call(name, arg) Scalar 调用已注册函数
py_list_functions() Scalar 列出已注册函数
py_agg(value, update, finalize) Aggregate JSON 状态字典聚合
py_scan(module, func) Table 调用 Python 函数返回行集
py_activate_venv(path) Scalar 激活 Python 虚拟环境
● ● ●

实战:分钟内从 CSV 到分析

LOAD 'python.duckdb_extension';

-- 注册转换函数
SELECT py_register('parse_date', 'x[:10]', 1);
SELECT py_register('to_upper', 'x.upper()', 1);

-- 直接 SQL + UDF 一条龙
SELECT
    to_upper(product),
    SUM(revenue)::INT as total
FROM read_csv('sales.csv',
    columns = {'date': 'VARCHAR', 'product': 'VARCHAR', 'revenue': 'DOUBLE'}
)
WHERE parse_date(date) > '2025-01-01'
GROUP BY product
ORDER BY total DESC;
不需要 pandas,不需要 Python 脚本——DuckDB 的 SQL 引擎 + Python 的表达能力,一个 LOAD 全搞定。 ● ● ●

架构:为啥直接调用之前会炸

整个项目最头疼的问题: py_register 注册的函数直接调用时 segfault。 原因 :DuckDB 的 init_extension 回调返回后,传入的连接句柄就失效了。 py_register 在查询回调里拿这个失效连接调 duckdb_register_scalar_function —— 必崩。 修复 :不在 init 缓存连接,而是存 database 句柄。每次 py_register 时从 database 开一条新连接,在新连接上注册函数,注册完马上断开。
init_extension → get_database(info) → DB (全局缓存)
py_register    → duckdb_connect(DB) → .register(con) → duckdb_disconnect
已验证平台:macOS arm64 + DuckDB 1.5.4 / 1.5.5。 ● ● ●

技术栈

  • quack-rs (Rust 写 DuckDB 扩展的框架)
  • PyO3 (Rust ↔ CPython FFI)
  • 约 1200 行 Rust,60 行 Makefile
● ● ●

兼容性

Python :扩展二进制不绑定特定版本——编译时用任意 Python 3,运行时 dlopen 当前环境的 libpython 。已验证 3.11 / 3.12 / 3.13(3.14 等 PyO3 升级)。
编译环境 运行环境 结果
Python 3.13 Python 3.11 ✅
Python 3.13 Python 3.12 ✅
Python 3.13 Python 3.13 ✅
DuckDB :与 Python 不同,DuckDB 扩展版本锁定——已验证 1.5.4 / 1.5.5。 项目地址:github.com/alitrack/duckdb-python ● ● ●

最后说两句

DuckDB 社区正在从"SQL 分析引擎"走向"可嵌入的分析平台"。 LOAD 扩展机制是通向这个未来的门—— httpfs 打通了存储, spatial 打通了 GIS, duckdb-python 打通了 Python 生态。 现在你可以在 SQL 里写 py_register('my_func', 'x**2 + 3*x + 1', 1) ,然后 SELECT my_func(5) ——一行注册,直接调用。如果能用 Python 表达,就能用 SQL 调度。