Andrei Lepikhov:为什么 PostgreSQL 的 numeric 类型如此之慢?

Andrei Lepikhov:为什么 PostgreSQL 的 numeric 类型如此之慢?

💡 原文英文,约6200词,阅读约需23分钟。
📝

内容提要

PostgreSQL 的 numeric 类型因值自描述、变长、精度与标度随数据动态确定等设计,性能低于 double:聚合慢近三倍、内存多九倍,且无法按值传递、元组解析更慢。DuckDB 改用整数存储、标度存于列类型、规划期定精度,以提升速度,但牺牲精确除法与超限安全。

🔎

延伸解读

numeric 慢在架构,不在算术

文章指出,PostgreSQL 的 numeric 类型性能问题并非源于十进制算术本身,而是源于四个刻意设计:表示不依赖声明、标度存于值内、结果标度由数据决定、精度上限灵活。这些决策保证了精确性和灵活性,但导致值自描述、变长、按引用传递,进而使聚合慢近三倍、内存多九倍。因此,优化方向应关注包装层(如内存分配、解包、标度计算),而非单纯改进算术。

DuckDB 的取舍:速度换精确

DuckDB 将 DECIMAL 存储为整数,标度存于列类型,规划期确定精度,从而获得接近整数的性能。但代价是:除法退化为浮点近似,AVG 返回 DOUBLE,且精度上限固定为 38 位,超出即报错。文章举例说明,DECIMAL(18,0) 相乘可能因结果超出容器而运行时溢出。这种设计适合 OLAP 场景,但对要求精确舍入的金融交易等场景可能不适用。

变长存储的连锁代价

numeric 是变长类型,导致元组解析(deform)时无法缓存列偏移,每行都需读取前序列头。文章引用 PostgreSQL 18 和 19 的改进(如 CompactAttribute)说明社区已关注此问题,但未根除。DuckDB 的列式存储虽无 deform,但压缩下宽类型(如 DECIMAL(19,2))解压成本显著,扫描耗时从毫秒级跃升至数百毫秒。两者都表明:将值送达操作的成本可能超过操作本身。

精确除法的硬约束

文章强调,精确十进制除法需要将操作数按结果标度扩展,例如计算 a·10^s/b。numeric 可动态增长数字数组,而固定宽度类型(如 int128)仅有 38 位,扩展空间有限。若参数精度加结果标度超过 38,则需 int256、报错或转浮点。DuckDB 选择浮点除法,牺牲精确性;PostgreSQL 则通过动态标度保持精确,但付出性能代价。这一矛盾在固定宽度设计中难以调和。

❓

Q&A

为什么 PostgreSQL 的 numeric 类型比 double 慢那么多?

PostgreSQL 的 numeric 类型设计为自描述、变长,精度和标度随数据动态确定,导致聚合操作比 double 慢近三倍,内存使用多九倍。具体原因包括:值按引用传递、元组解析更慢、中间结果标度需动态计算等。

PostgreSQL 的 numeric 类型有哪些设计决策导致性能问题?

PostgreSQL 的 numeric 类型有四个关键设计决策:1) 表示不依赖于声明,宽度是类型属性,因此必须变长;2) 标度存储在值中而非列描述中;3) 结果的标度由数据计算而非预先确定;4) 精度上限灵活,运行时几乎不会溢出。这些决策保证了精确性,但带来了性能开销。

DuckDB 的 DECIMAL 类型是如何实现高性能的?

DuckDB 将 DECIMAL 存储为整数,标度存储在列类型中,精度在规划阶段确定。它使用 INT16、INT32、INT64、INT128 作为载体,根据声明宽度选择,使得算术运算简单快速。但牺牲了精确除法和超限安全性,除法使用浮点近似,且没有 NaN 和无穷大。

PostgreSQL 的 numeric 类型在聚合操作中为什么内存消耗大?

因为 numeric 是变长且自描述的,每个值都携带标度等信息,聚合时需要更多内存来存储中间结果。例如,对 12 个 numeric 列进行分组聚合,内存使用可达 1.8GB,而 double 仅需 196MB。此外,sum(numeric) 使用 NumericSumAccum 结构,用 32 位整数存储数字,正负值分开累加,也增加了内存开销。

将 numeric 存储为 int128 能解决性能问题吗?

不能完全解决。虽然 int128 可以加速一些操作,但除法仍然昂贵,因为 128 位除法需要调用库函数。此外,精确除法需要扩展被除数,而 int128 只有 38 位数字,容易溢出。打印也会变慢,因为需要将二进制转换为十进制。因此,优化应关注外围操作如内存分配、解包和标度计算。

PostgreSQL 和 DuckDB 在处理 decimal 除法时有什么不同?

PostgreSQL 的 numeric 除法会动态选择结果标度,保证至少 16 位有效数字,结果精确。而 DuckDB 的 DECIMAL 除法直接使用浮点近似,返回 DOUBLE 类型,因为固定点小数除法通常不产生有限小数。这导致 DuckDB 无法满足需要精确除法的场景,如金融计算。

🏷️

标签

➡️

继续阅读