PostgreSQL码农集散地

PG 19 tuple deformation 优化一镜到底

本期播客

PG 19 tuple deformation 优化一镜到底

如果你的 SQL 已经不慢了,为什么 CPU 还是高?
答案可能不在执行计划里,而在 PostgreSQL 每取出一行数据时,都要偷偷做的一件事:tuple deformation。

PostgreSQL 19 又把一段“隐形 CPU 黑洞”砍掉了:这次不是 SQL 优化,是每一行数据都要走的底层路径变快了

很多 DBA、架构师、开发者看 PostgreSQL commit,习惯先找这几个关键词:Join、Scan、Index、WAL。
但 2026 年 3 月 15 日这个 commit,值得你停下来认真看一眼:

c456e39113809376f6604e720910ccd24e18e034
标题很朴素:Optimize tuple deformation

朴素到容易被低估。
但我要直接下判断:

这不是“小修小补”,而是 PostgreSQL 在削一段“每行都要经过”的公共 CPU 路径。
它影响的不只是数据库内核跑分,还会影响真实业务里:

  • 大量返回结果集的查询
  • 宽表读取
  • SELECT *
  • ORM 自动生成 SQL
  • 高频 UPDATE
  • PL/pgSQL 逐行处理
  • 某些表重写、逻辑变更、批处理场景

说得更狠一点:

很多系统不是死在“查不到”,而是死在“每查到一行,都要拆一遍”。

一、什么是 tuple deformation?为什么你该关心?

先把概念说人话。

PostgreSQL 里的行,不是按“Java 对象”那样整齐摆好的。
它在磁盘和内存里的物理表示,是一种尽量紧凑、节省空间、支持 NULL、支持变长字段、支持对齐的压缩布局。

但执行器真正要做计算时,比如:

  • 读 WHERE 条件里的列
  • 计算 SELECT 输出列
  • 做表达式、函数、聚合
  • 更新某些字段
  • 把一行塞进 TupleTableSlot

它不能直接拿这坨紧凑字节流开算。
它得先把这一行“拆”成一个个字段值和 isnull 标记。
这个拆解过程,就是 tuple deformation。

DeepWiki 对 PostgreSQL 内核的总结很直接:
tuple deformation 是把物理 tuple 解包成执行器可直接使用的 Datum[] 和 isnull[]。
它广泛出现在表达式求值、TupleTableSlot 填充、UPDATE、PL/pgSQL、表重写等路径中,而且在 CPU profile 里经常是热点。

这就引出第一性原理:

只要你的 workload 是“处理很多行、访问很多列、CPU 比 I/O 更紧张”,tuple deformation 就天然可能成为瓶颈。

所以,这个 commit 的价值,不是“某个冷门函数快一点”,而是:

每一行被执行器消费之前,底层那段拆包逻辑更高效了。

PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!

图片

二、这次优化到底做了什么?一句话概括

一句话概括:

PostgreSQL 把原来很多“边拆边判断、边走边算”的 tuple deformation 路径,改成了更多“预先算好、批量判断、少分支、少逐字段试探”的路径。

commit 里最关键的几个点,有四层:

1. 预先计算属性偏移,不要每次拆 tuple 时再算

这次把 CompactAttribute 的 attcacheoff 提前预计算了,交给 TupleDescFinalize() 去做。
这意味着:很多固定布局的列,不需要等到运行时在 deform 逻辑里再反复推 offset。

翻译成工程语言就是:

把运行期重复劳动,前移成描述符初始化阶段的一次性劳动。

这类优化最值钱的地方,不是单次节省几条指令,
而是它处在“每 tuple、每 attribute”都会经过的热路径上。
乘数一大,收益就会很真实。

2. NULL 检查不再一位一位抠,而是一字节一字节扫

过去如果 tuple 带 NULL,代码会一位一位检查 NULL bitmap。
现在改成了按字节处理,先更快找到第一个 NULL 在哪里,然后在那之前尽可能走无需逐列 NULL 判断的快路径。

这背后的原理非常简单:

分支少、循环次数少、位图扫描粒度更大,CPU 更开心。

尤其在现代 CPU 上,细碎、不可预测的小分支,常常比你想象的更贵。

3. 引入“保证存在的列”概念,减少无意义检查

commit 还记录了一个“最大 guaranteed attribute number”,也就是:
哪些列是一定存在、受 NOT NULL 约束、且不是 atthasmissing 这类特殊情况。

这带来的意义很大:

  • 访问这些列时,不必每次回头看 tuple 的 natt
  • 某些路径不用额外做 byref 获取相关准备
  • 对固定宽度列的拆解,可以更直接

这是一种典型的数据库内核优化思路:

把 schema 层面的约束,转化为执行期的剪枝条件。

也就是说,约束不只是保证数据正确,
还可以变成性能优化前提。

4. isnull[] 填充用了 SWAR 技巧,一次处理 8 个

这次 commit 还用了一个很硬核、但很实用的优化:
通过位运算和 SWAR 技巧,一次处理 8 个 isnull 元素,而不是一个个通过 att_isnull() 去算。

这类优化的本质不是“炫技”,而是:

把小而频繁的标量判断,改造成更接近批处理的位运算。

对于高频热路径,这种改法很容易换来实打实的吞吐提升。

三、为什么说它重要?因为它打中的不是“少数 SQL”,而是“公共底座”

很多人看内核优化,最容易犯的错就是:
“这不就是内部细节吗?业务 SQL 又没改。”

这恰恰是误判。

因为 tuple deformation 不是某类高级特性才会走的路径,
它是 PostgreSQL 执行器消费行数据时的公共底座。

只要满足下面这些条件,它就很容易成为显著成本:

  • 查询返回很多行
  • 每行访问很多列
  • 宽表较多
  • 行里有变长字段、NULL 字段
  • CPU 已经比磁盘更先打满
  • 应用层拿到的是“整行”而不是极少数字段

这正是很多现代业务系统的真实形态:

  • ORM 默认查出一堆列
  • 业务接口懒得裁剪字段
  • 宽表承载订单、用户画像、风控标签、审计字段
  • 中间层把数据库当对象存储来用
  • 批处理、ETL、逻辑复制周边程序频繁读整行

所以,这个 commit 优化的是一种“不会写在 SQL 里,但会体现在 CPU 账单上”的成本。

四、这次性能提升有多大?别夸张,但也别轻视

commit 作者写得非常克制,也非常关键:

  • Apple M 系列芯片对这组改动特别“买账”
  • 某些以 deformation 为重的测试查询,速度超过 2 倍
  • 在 x86-64 上,提升没有那么夸张,但仍有收益
  • 列数少的小表,收益会更有限

这段话非常值得尊重,因为它避免了数据库圈最常见的误导:
拿某个平台、某个 microbenchmark 的最好结果,包装成普遍真理。

所以更严谨的结论是:

这不是“所有查询都翻倍”,而是“在 tuple deformation 占比较高的场景里,某些平台可以非常可观;其他平台也有稳定但没那么戏剧化的收益”。

这才是专业判断。

五、DBA、架构师最该看到的,不是“快了”,而是“什么场景会快”

对 DBA 和架构师来说

你最该记住的不是某个函数名,而是这条判断公式:

如果数据库已经从 I/O 瓶颈转向 CPU 瓶颈,那么执行器热路径上的微观优化,会越来越值钱。

过去十几年,很多数据库调优都围绕:

  • 索引有没有
  • SQL 改没改
  • I/O 能不能扛住
  • shared buffers 多不多

但今天的硬件和业务形态变了:

  • NVMe 和大内存把大量随机 I/O 压下去了
  • 查询慢,越来越常常不是磁盘先爆,而是 CPU 先红
  • 特别是在 API 型业务、HTAP 边缘、微服务多小查询场景里,CPU hot path 的边际价值越来越高

所以这次优化的战略含义是:

PostgreSQL 社区在持续把“每 tuple 的固定 CPU 成本”往下压。

对架构师来说,这意味着版本升级的收益,不能只看 planner feature list,
还要看这种执行器/slot/tuple 路径的“基础设施级瘦身”。

对应用开发者来说

这次 commit 其实也在“打脸”一种坏习惯:

你写的 SELECT *,从来不只是网络多传几个字段而已。

字段一多,tuple deformation 就更重。
NULL 一多,变长列一多,旧路径就更容易慢。
ORM 把一个实体 40 个字段全查出来,哪怕你只用了 3 个,这个底层拆包成本仍然可能已经付掉了。

所以对开发者,这个 commit 传递的不是“以后随便 SELECT * 也没事”,恰恰相反:

数据库内核已经在拼命替你省 CPU 了,但你不能把浪费当红利。

六、第一性原理:什么时候这个优化最有价值?

这篇文章必须把前提讲清楚,否则就是在制造幻觉。

这个优化最有价值,依赖于以下前提:

前提 1:你的 workload 里,tuple deformation 真的是显著成本

典型特征是:

  • CPU profile 里 executor/slot/deform 路径有存在感
  • 查询返回行数大
  • 每行访问列数多
  • 宽表、NULL、变长字段较多

如果这个前提成立,优化价值很大。

前提 2:系统更偏 CPU-bound,而不是 I/O-bound

如果你的系统主要卡在:

  • 磁盘随机读
  • 网络传输
  • 锁等待
  • WAL fsync
  • 远程存储延迟

那 tuple deformation 再快,也只是次要收益。
因为主矛盾不在这里。

前提 3:查询确实需要把列“拿出来用”

如果你主要是:

  • index-only scan
  • 非常窄的投影
  • 只取少数几个固定列
  • 很少返回结果
  • 大部分耗时在 join、sort、agg、network 上

那这个优化的边际收益自然会下降。

七、如果前提崩塌,结论也要跟着变

专业判断的关键,不是会喊结论,而是知道结论何时失效。

情况 1:如果你的表很窄,列很少

那这次优化大概率不会带来戏剧性改变。
commit 自己已经说了:列数少时,收益更小。

这时候更重要的,往往还是:

  • SQL 写法
  • 索引设计
  • 连接池
  • 锁竞争
  • 批量化程度

情况 2:如果你的系统主要卡 I/O

那你更该优先关心:

  • 扫描方式
  • 缓存命中率
  • autovacuum
  • checkpoint/WAL
  • 表膨胀和索引膨胀

而不是把希望押在 deformation 优化上。

情况 3:如果你的应用过度依赖宽表和 SELECT *

那正确观点不是“PostgreSQL 已经优化了,所以可以继续乱取列”,
而应该是:

内核优化只是止损,不是给坏建模和坏访问模式兜底。

情况 4:如果某些 tuple 还没完成 NOT NULL 约束验证

commit 也明确提到:
某些路径上,优化不能默认使用,需要 TupleTableSlot 显式带上 TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS。

这很说明 PostgreSQL 的一个工程风格:
宁可保守启用,也不为了性能破坏语义正确性。

八、这件事对版本升级的启示,比这一个 commit 更大

真正值得 DBA 和架构师重视的,不只是这个 patch 本身,
而是它体现出的 PostgreSQL 演进方向:

社区已经越来越多地在啃“每行成本”“每 tuple 成本”“每个 executor 热循环成本”这种硬骨头。

这意味着什么?

意味着数据库性能竞争,正在从“有没有大特性”,转向“基础路径瘦不瘦、稳不稳、是否持续被现代 CPU 友好化”。

你可以把它理解成一句话:

PostgreSQL 不只是会让计划更聪明,也在让每一行数据被消费得更便宜。

这对真实生产是大事。
因为在海量 API 调用、复杂 ORM、宽表现实、CPU 成本越来越贵的环境里,
这种底层优化最终会沉淀成:

  • 更低 CPU 占用
  • 更高吞吐
  • 更好的尾延迟
  • 更晚到来的扩容节点

九、给两类读者的直接建议

给 DBA / 架构师

如果你们的系统已经进入 CPU 紧张期,升级 PostgreSQL 时不要只盯 SQL 层 feature。
执行器热路径、tuple/slot/JIT/AIO 这类“看上去不性感”的改动,往往才是真正能把机器再榨出 10% 到 30% 价值的地方。

尤其是:

  • 宽表业务
  • 大量 API 式短查询
  • 高并发读取
  • ETL/批量更新
  • ORM 重度使用

这些场景值得重点关注。

给应用开发者 / 数据库用户

请把这次 commit 当成一个提醒,而不是免罪金牌:

  • 少写 SELECT *
  • 少把宽表当默认设计
  • 明确投影字段
  • 注意 NULL、变长列、超宽实体的代价
  • 不要以为“查询走到数据库了,剩下都是免费的”

数据库每返回一行,都是有拆包成本的。你少取一列,很多时候不是“省一点网络”,而是省掉整条底层处理链上的一段工作。

结论

这次 Optimize tuple deformation,本质不是“某个函数更快了”。
它真正说明的是:

PostgreSQL 正在持续压缩“每行数据被使用一次”的固定 CPU 成本。

这对 DBA、架构师、开发者都有一个共同启示:

  • 数据库性能,不只取决于“有没有命中索引”
  • 还取决于“命中之后,每一行要付出多少 CPU 代价”
  • 而这类代价,往往恰恰最容易被应用层的宽表、NULL、SELECT *、ORM 滥用放大

所以,这个 commit 最值得记住的一句话是:

未来 PostgreSQL 的竞争力,不只是把计划做对,更是把每一行的处理成本做低。


你怎么看

你在生产里更常见的瓶颈,是 I/O、锁,还是这种“不容易在 SQL 文本里直接看见”的 CPU 热路径?
你们团队会刻意约束 SELECT *、宽表和 ORM 默认取全列吗?
评论区聊聊:你觉得这类底层优化,对业务真实收益大,还是“看起来高级、体感有限”?