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 调度。