当前位置:首页>python>在 SQL 里直接写 Python——我做了一个 DuckDB 扩展

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

  • 2026-10-11 06:19:36
在 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 totalFROM read_csv('sales.csv',    columns = {'date': 'VARCHAR', 'product': 'VARCHAR', 'revenue': 'DOUBLE'})WHERE parse_date(date) > '2025-01-01'GROUP BY productORDER 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 调度。

最新文章

随机文章