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