就在这周,AWS 宣布已签署最终协议,收购位于阿姆斯特丹的 DuckLabs——DuckDB 背后的公司。DuckLabs 团队将整体加入 AWS。 DuckDB 本身继续留在独立的非营利组织 DuckDB Foundation 名下,继续以 MIT 协议免费开源;DuckDB 的创造者、DuckLabs 联合创始人 Hannes Mühleisen 和 Mark Raasveldt 将继续带领团队,并继续主导开源项目的技术方向。
DuckDB 是一个进程内的列存分析型数据库,常被称作「分析领域的 SQLite」。它没有服务端,不需要独立部署,以库的形式被宿主程序链接进去。而正是这个形态,让它走出了一条和其他分析引擎都不同的路。
PostgreSQL 是典型的行存 OLTP 引擎,它在事务处理上非常强,但跑大规模聚合、全表扫描、大 JOIN 时,比不过列存向量化引擎——这不是实现好坏的问题,是存储布局和执行模型决定的。
长期以来这个缺口靠 ETL 填:把数据同步一份到外部数仓,交易归交易,报表归报表。这也意味着数据副本、同步延迟。
而 DuckDB 恰好是列存向量化引擎里最特别的一个:它是进程内的,以库的形式存在,没有服务端、不需要独立部署。 一个不需要独立部署的分析引擎,天然就适合被塞进另一个数据库里。 这个想法并不难想到,难的是把它做出来。
2024 年 8 月,一个把 DuckDB 嵌入 PostgreSQL 的开源协作项目公开亮相,参与方是三家: Hydra、DuckDB Labs、MotherDuck。项目最初由 Hydra 发起,他们此前一直在做 PostgreSQL 上的分析扩展,贡献了 PG 扩展与存储方面的工程经验;DuckDB Labs 提供引擎侧的支持;MotherDuck 则把它接进了自己的云服务。
三家凑到一起并不是巧合——它们面对的是同一个需求:Hydra 要给 PG 用户提供分析能力,MotherDuck 要让 DuckDB 能对接存量的 PG 数据,DuckDB Labs 需要一条通往 OLTP 世界的入口。各做各的,不如合做一个。
项目最终落在了 DuckDB 自己的 GitHub 组织下——
github.com/duckdb/pg_duckdb。这就是「官方扩展」的实际含义:不是一个第三方项目被认可,而是它本身就归属在 DuckDB 组织内维护。
总结来说就是 DuckDB 是引擎,pg\_duckdb 是引擎的外壳。
third_party/duckdb,指向
github.com/duckdb/duckdb.git),版本在 Makefile 里被钉死(
DUCKDB_VERSION,随上游主线滚动升级),编译后与扩展代码一起链进同一个
.so。CREATE EXTENSION pg_duckdb 之后,每个 backend 内部就多了一个可用的分析引擎。分工也很清晰:事务、权限、catalog、存储、连接管理仍然全部归 PostgreSQL;DuckDB 只负责列存向量化执行。用户侧的开关很简单:
SET duckdb.force_execution = true;
之后不改一行 SQL,符合条件的查询就会走 DuckDB 执行;不支持的查询自动回退到 PostgreSQL 原生执行器。
理解 pg\_duckdb 的实现,最关键的一点是: 它不是把 PostgreSQL 的计划树翻译成 DuckDB 的计划树,而是把查询反解析成 SQL 文本,交给 DuckDB 重新解析。
这个「SQL 文本往返」是整个架构的核心,也是后面所有适配工作的根源。完整链路如下:
1. Hooks 拦截查询
_PG_init 时接管三个 hooks(
src/pgduckdb_hooks.cpp):
planner_hook、
ExecutorStart_hook、
ExplainOneQuery_hook。查询进入规划阶段时先过一遍判定:是否开启了
force_execution、是否引用了 DuckDB 才有的表函数(
read_parquet 等)、是否命中不支持的类型或语法。不合格就原样交还
standard_planner,用户全程无感知。
2. 反解析成 DuckDB SQL
命中之后,
pgduckdb_planner.cpp 调用
pgduckdb_get_querydef()(
src/pgduckdb_ruleutils.cpp,是 PostgreSQL
pg_get_querydef 的定制版本),把
Query 树重新渲染成一段 SQL 字符串,再交给 DuckDB 的
Connection::Prepare()。DDL 路径类似,
pgduckdb_get_tabledef() 负责把 PG 表定义渲染成 DuckDB 的
CREATE TABLE。
3. 用 CustomScan 节点承载执行
规划结果是一个
CustomScan 节点(
src/pgduckdb_node.cpp),实现了
BeginCustomScan /
ExecCustomScan /
EndCustomScan /
ExplainCustomScan 一整套回调。对 PostgreSQL 的执行器而言,DuckDB 只是一个普通的扫描节点:每次
ExecCustomScan 从 DuckDB 拉一批结果,转成
TupleTableSlot 往上吐。这也是
EXPLAIN 能同时展示两边计划的原因。
4. 反向读回 PostgreSQL 堆表
数据不需要导出。DuckDB 侧通过 replacement scan 机制注册了对 PG 表的访问(
src/scan/postgres_scan.cpp、
postgres_table_reader.cpp):当 DuckDB 解析到一个它 catalog 里没有的表名时,回调进 pg\_duckdb,由后者直接走 PostgreSQL 的缓冲区管理器和快照读堆表。过滤条件会被下推回 PG 侧扫描(
ExtractQueryFilters),并且支持拉起 PostgreSQL 并行工作进程来喂数据。
5. 类型双向映射
src/pgduckdb_types.cpp 负责两个方向的转换:PG OID → DuckDB
LogicalType,以及 DuckDB 值 → PG
Datum。这是整个扩展里最琐碎也最容易出问题的一块——两边的类型系统并不同构(典型如无精度约束的
NUMERIC,默认会告警回退)。
另外还有
src/catalog/ 下的一组文件,让 DuckDB 的事务管理器与 PostgreSQL 的事务边界对齐;
src/vendor/ 则是从 PostgreSQL 和 DuckDB 内部抄出来的头文件——这也说明了两个项目的耦合有多紧:DuckDB 的 C++ 内部 API 并不承诺稳定,每次升级 DuckDB 版本,pg\_duckdb 往往都要跟着改代码。
IvorySQL 是基于 PostgreSQL 的 Oracle 兼容数据库,在内核里实现了一整套 Oracle 兼容的数据类型和语义。这些类型是 IvorySQL 自己注册的,OID 是 动态分配的——而 pg\ duckdb 的类型映射大量依赖硬编码的内建 OID 判断。直接拿上游 pg\duckdb 用在 Oracle 模式下,结果就是:要么大面积回退、失去加速,要么在类型转换路径上直接报错。
于是有了
ivy_duckdb(
https://github.com/IvorySQL/ivy\
duckdb
),IvorySQL 维护的 pg\duckdb 分支。当前基线是上游 pg\_duckdb 1.1.0 + DuckDB v1.4.3,相对上游约 2500 行改动,集中在
src/、
include/、
test/ 三处。主要工作分成四块:
新增
Ivory*Oid() 一族接口(
include/pgduckdb/pgduckdb_metadata_cache.hpp),覆盖 IvorySQL 中实现的 Oracle 兼容类型,通过 catalog 查找拿到运行时 OID 并缓存起来,随 catalog 失效自动更新。类型映射的判据从「硬编码 OID」改成「查缓存」,这是所有后续工作的地基。
ivory:oradate),PG 侧重新解析时在
postgres_scan.cpp 里做 alias 剥离后再渲染。pgduckdb_ruleutils.cpp 做文本改写 ,并通过
ExtensionLoader::RegisterType() 把全部 Oracle 兼容类型正式注册进 DuckDB 的系统 catalog。OFFLINE=1 关闭 httpfs 的 FetchContent 拉取,预安装 Duckdb 扩展),适配内网环境。
ivy_duckdb 本身是一个扩展,需要自行编译安装。对想快速上手的用户,更方便的入口是
ivy_mooncake(
https://github.com/IvorySQL/ivy\_mooncake )——IvorySQL 维护的
pg_mooncake 发行版,它把
ivy_duckdb 直接作为子模块包含在内。
pg_mooncake 本身是给 PostgreSQL 表建立 Iceberg 列存镜像的扩展:行存表照常承担 OLTP 写入,镜像表以亚秒级新鲜度同步,分析查询打在列存上,加速引擎正是 DuckDB。它的扩展控制文件里写着
requires = 'pg_duckdb'——也就是说,
ivy_mooncake 天然依赖
ivy_duckdb,两者是一套东西的两层:
ivy_duckdb —— 查询加速层。行存表不动,符合条件的查询交给 DuckDB 执行。ivy_mooncake +
ivy_moonlink —— 列存镜像层。通过逻辑复制把行存表增量同步成 Iceberg 列存,分析查询直接打列存。最快的体验方式是预览镜像,里面已经预装并预加载好了 IvorySQL +
pg_duckdb +
pg_mooncake:
docker run --name ivy_mooncake \ -e IVORYSQL_PASSWORD=password \ -p 5432:5432 -p 1521:1521 \ -v ivy_mooncake_data:/var/lib/ivorysql/data \ -v ivy_mooncake_warehouse:/tmp/moonlink_iceberg \ registry.highgo.com/mooncake/ivy_mooncake:0.1
起来之后三条语句就能看到效果:
CREATE EXTENSION pg_mooncake CASCADE; -- CASCADE 会一并装上 pg_duckdb -- 开启查询加速,然后查原表即可 SET duckdb.force_execution = true;
把上面这些工作串起来看,结论其实很简单: IvorySQL 在保持 Oracle 兼容的前提下,获得了原生的 OLAP 分析能力。
ivy_duckdb 的查询加速:
SET duckdb.force_execution = true,SQL 一行不改,直接吃到列存向量化执行;重量档是
ivy_mooncake 的 Iceberg 列存镜像:给热表建一个自动同步的列存副本,分析查询打在副本上,行存侧的 OLTP 负载不受影响。两档能力的详细用法——列存镜像的建立与同步、加速路径的开关与回退规则、Oracle 类型的支持范围——参见 IvorySQL/ivy\ mooncake ( https://github.com/IvorySQL/ivy\ mooncake) 仓库文档。
我们在 IvorySQL 的 Oracle 兼容模式和 pg 模式下都做了验证,用 TPC-H 查询集对三条执行路径做了同机横比:行存原生执行、
ivy_duckdb 查询加速、
ivy_mooncake 列存镜像。结论是清晰的量级差异——查询加速相对原生执行有明显提升,列存镜像则再上一个台阶,两者的差距在大数据量、重扫描的聚合类查询上尤其突出。
回到开头:AWS 收购 DuckLabs,改变的是 DuckDB 的资源与商业化路径,没有改变它的许可证和治理结构。真正烧到 PostgreSQL 的,不是这桩交易本身,而是它背后那个已经成立的事实——列存分析引擎可以被装进 OLTP 数据库,而且已经有人跑通了。对 PostgreSQL 生态而言,更值得关注的是 pg\_duckdb 这条已经跑通的路径:不改 SQL、不搬数据、同进程嵌入,把列存向量化能力直接补进 OLTP 数据库。
目前 ivy\ mooncake/ivy\duckdb 发布的是 v0.1 预览版,欢迎大家使用。如果遇到问题,欢迎到仓库提 issue:
参考链接
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70043484/viewspace-3181003/ ,如需转载,请注明出处,否则将追究法律责任。