alitrack

DuckDB 2.0 升级完测试全绿,数字已经变了

DuckDB 2.0 在 10 月 21 日发布了,LTS 长期支持版,代号 Cinnamon Teal。升级公告里最醒目的提醒是:老版本打不开新版本建的库文件。很多人看完这段就把升级跑了,测试全绿,然后收工。

问题在于,有一类变更不报错、不警告、不崩溃,只是把查询结果悄悄换掉。你的测试只断言「没抛异常」,它当然全绿。数字对不对,另说。

我在本机装了 1.5.6 和 2.0.0 两个版本,把官方迁移文档里列出的变更逐条跑了一遍。先说结论:能一分钟修好的坑,DuckDB 会大声告诉你;改你数字的坑,它一声不吭。

● ● ●

会报错的坑:存储版本 69

2.0 新建的数据库文件,存储版本号是 69。用 1.5.6 去打开,直接报错:

IO Error: Trying to read a database file with version number 69, but we can only read versions between 64 and 68.

这个错我在本机逐字复现了。它吓人但不危险:失败发生在打开文件那一刻,一行配置就能绕过。写入端设置 storage_compatibility_version 为 v1.5.0,写出来的文件老版本照样能读,我也实测验证了。

真正的风险在混合部署:团队里有人先升了 2.0,新建的文件别人打不开。CI 在 Python 3.12 上跑得欢,生产环境还停在 3.10,更要命的是 2.0 要求 Python 3.11 以上,3.10 上执行 pip install duckdb 会静默装回 1.x,没有任何提示。版本分叉就这么发生了。

顺带说一句时间线:1.4 LTS 今年 11 月 17 日停止支持,1.5 延到了明年 3 月。没人逼你现在升。

● ● ●

不报错的坑:六条悄悄换答案的 SQL

下面每一条,都是我在 1.5.6 和 2.0.0 上各跑一遍、结果摆在一起的。

第一条,正则匹配运算符换了语义。

SELECT 'abc' ~ 'b';

1.5.6 返回 false,2.0 返回 true。波浪号从「全串匹配」改成了 PostgreSQL 式的「部分匹配」。如果你的过滤条件里用了波浪号,升级后保留的行会变多。数据分析师拿旧报表一对数,以为出了 bug,其实是引擎换了规矩。临时回退可以用 SET regex_match_operator_semantics = 'full',但官方标了 deprecated,早晚要改。

第二条,阶乘的绑定变紧了。

SELECT 2 * 3!;

1.5.6 算出 720,它把 2 乘 3 先算完再阶乘。2.0 算出 12,阶乘先算,再乘 2。数学上 2*3! 本来就该是 12,这次是改对了,但改对也是改。

第三条,十六进制字面量换了类型。

SELECT typeof(X'41');

1.5.6 给 VARCHAR,2.0 给 BLOB。B'...' 同理变成 BITSTRING。下游如果按字符串处理这个值,行为就变了。

第四条,Python 端 Decimal 的精度。

Decimal("1E+2") 传进去,1.5.6 里是 1.00,类型 DECIMAL(3,2);2.0 里是 100,类型 DECIMAL(3,0)。小数点挪了两位,这要是出现在金额字段上就是事故。

第五条,Python 表达式 API 里裸字符串的含义。

ColumnExpression("s") == "x",1.5.6 把 "x" 当列名,2.0 把它当字面值,只弹一个 warning 不拦你。写法要改成 col("x") 引用列、lit("x") 引用文本。

第六条,Arrow 批次默认行数从一百万降到 131072。依赖批次大小的代码,内存占用和吞吐都会跟着变。

这六条共同点:不报错,只换结果。

● ● ●

反着来的坑:以前静默,现在报错

还有一批变更方向相反,从静默成功变成直接抛错。整除和取模除以零,1.5.6 返回 NULL,2.0 报错。MERGE INTO 一条语句更新同一目标行两次,以前随便挑一条用,现在报错。CREATE TABLE AS SELECT NULL 建出来的列,以前是 INTEGER,现在是 NULL 类型,再往里插数字会被拒。

这些至少会在测试里炸出来,属于好修的那类。

● ● ●

帖子没说、我实测又踩到的

官方迁移文档比那张流传的截图更细。我照着文档把 Python 客户端的变更也测了一遍,又有几条收获。

relation 对象不再缓存结果。fetchone、fetchmany、rel.execute 这些方法直接没了,每次读取都会重新执行查询。我的测试脚本第一版就是在 v2.0 上因为 fetchone 不存在而崩掉的。

事务里任何一条语句失败,整个事务作废。1.5.6 里绑定错误之后还能继续写、还能 commit,2.0 里 commit 直接抛 TransactionException。依赖「失败了重试一下」的逻辑要重写。

write_csv 指定 overwrite=False 时,老版本会静默覆盖已有文件,2.0 改成抛 IOException。这个算是把以前的坑填了,但如果你有代码依赖旧的静默行为,会突然断掉。

还有 con.sql 跑 DDL 语句,1.x 返回 None,2.0 返回一个 relation,在 notebook 里每次显示都会把语句重跑一遍。以及用点号引用列的写法 ColumnExpression("t.x"),现在要求写成 col("t", "x"),否则按单个列名处理直接报绑定错误。

● ● ●

升级前值得做的三件事

第一件,锁版本。所有碰过同一个 duckdb 文件的进程,统一锁死同一个版本号;写入端设置 storage_compatibility_version,等最后一个读者升完再放开。

第二件,扫 SQL。在代码库里全文搜索波浪号、感叹号、X' 和 B' 这几个模式,重点看过滤条件。光扫还不够,挑几条关键查询,在 1.5.6 和 2.0 上各跑一遍,对比行数和校验和。

第三件,别信退出码。一个只检查异常的升级测试,在 2.0 上会全绿通过,然后把不同的数字发上线。要测的是结果本身。

官方自己的建议也直白:关键业务等 11 月的第一个 patch 版本再说。

● ● ●

这事的源头

这个清单的原始出处,是伦敦数据工程顾问 Firat Celik 发在 LinkedIn 上的一篇帖子。我把帖子里的十二条主张跟官方迁移文档逐条比对,全部属实,没有夸大。本文的实测环境和完整对拍矩阵,都可以按文中给出的 SQL 复现。

DuckDB 2.0 本身是个大版本:异步 I/O、VARIANT 类型、触发器、还有给编程 agent 用的 CLI 模式。值得升,但值得带着对拍清单升。

● ● ●

参考来源

https://duckdb.org/2026/10/21/announcing-duckdb-200.html

https://duckdb.org/docs/2.0/operations_manual/migration.html

https://duckdb.org/docs/2.0/clients/python/migration

https://www.linkedin.com/posts/fircelik_duckdb-dataengineering-python-share-7520730421031170048-Ebl7