来源:
https://www.pgedge.com/blog/why-is-numeric-so-popular-in-postgresql-databases

为什么 numeric 在 PostgreSQL 数据库中如此流行?

Andrei Lepikhov | 2026年9月3日

PostgreSQL 关于 numeric 的文档中包含了两句不太和谐的话:

“特别推荐用于存储货币金额和其他需要精确性的数量”——紧接着却是:“与整数类型或浮点类型相比,对 numeric 值的计算非常慢”。因此,该类型被推荐用于存储货币金额,但同时又承认它相当昂贵。

对我来说,作为一名 DBMS 开发者,这读起来像是一个行动号召。如果对一种类型的操作明显慢于对 bigint 的操作,那么一个诱惑就会浮现:我们能否将货币金额存储为整数形式的美分数,并按标准规则进行舍入?这将在我们的数据库服务器上节省相当多的计算资源,不是吗?如果我们更进一步,使用 double precision 呢?

但在优化该类型或将其替换为整数之前,值得了解实际对其的要求是什么:法律要求什么,数据交换格式要求什么,应用平台要求什么。精确小数类型真的是金融应用的标准(即使是事实上的标准)吗?还是这仅仅是一种可以安全绕过的工程学传说?

与其依赖调查文献,不如让我们深入挖掘一手资料。这项任务从来都不简单,但 AI 智能体使其变得容易得多。那么,让我们卷起袖子开始吧。如果文本感觉过于枯燥或无聊——嗯,那是因为它确实如此。这就是为什么有一个目录,你可以快速跳转到你需要的内容。

目录

  1. SQL 标准怎么说
  2. 法律和监管机构要求什么
  3. 金融数据交换格式
  4. 支付系统:小数位数作为货币的一个属性
  5. TPC 基准测试要求什么
  6. 供应商、权威机构和实践者怎么说
  7. ERP 系统中发生了什么
  8. 精确小数类型的发展方向
  9. 结论

1. SQL 标准怎么说

ISO SQL 没有 MONEY 类型。而且不仅仅是缺少类型——SQL 标准完全没有货币的概念。PostgreSQL 中的 money 类型和 SQL Server 中的 money/smallmoney 是供应商扩展,而非标准的实现。

标准确实包含三类数值类型:

  • 精确数值类型NUMERICDECIMALSMALLINTINTEGERBIGINT
  • 近似数值类型FLOATREALDOUBLE PRECISION
  • 十进制浮点类型DECFLOAT

NUMERICDECIMAL 之间有一个微妙的语义差异。

子条款 6.1“数据类型”,语法规则 28 和 29:

NUMERIC 指定了精确数值数据类型,其十进制精度和小数位数由指定的精度和小数位数决定。29) DECIMAL 指定了精确数值数据类型,其十进制小数位数由指定的小数位数决定,而其实现定义的十进制精度等于或大于指定精度的值。

为了具体说明差异:NUMERIC(15,2) 是精确 15 位数字的硬限制,而 DECIMAL(15,2) 表示“不少于 15 位”。根据这个定义,这两种类型可以合并为一种——这正是 PostgreSQL 的 numeric 所做的。

标准并不禁止在二进制整数之上实现精确小数类型。基于 int64/int128 的固定宽度是标准明确允许的一种选项。这使得 Arrow、SQL Server、DuckDB 等中的实现变体成为可能。

子条款 4.5.2 - 数字的特性:

精确数值类型具有精度 P 和小数位数 S。P 是一个正整数,它决定了在特定基数 R(R 为 2 或 10)中有效数字的位数。S 是一个非负整数。小数位数为 S 的精确数值类型的每个值都采用 n × 10⁻ˢ 的形式,其中 n 是一个整数,使得 −Rᴾ ≤ n < Rᴾ。

底线:精确小数类型 DECIMAL/NUMERIC 属于 SQL 的核心强制部分(特性 E011-03),而 DECFLOAT 是可选的(特性 T076)——顺便说一句,BIGINT 也是可选的。

还有一个点对分析 numeric 的要求很重要:SQL 标准对最小或最大精度没有规定,但它确实约束了算术运算——加法、减法和乘法的小数位数规则被严格固定。除法的小数位数留给实现定义。

2. 法律和监管机构要求什么

关于引入欧元的欧盟理事会条例 (EC) No 1103/97 考虑得很周全:它相当明确地要求六个有效十进制位,不得四舍五入或截断,并确定了在该精度之外舍入时的确定性行为。浮点类型根本不符合这些要求。

条例第 4 条定义了转换机制本身:汇率取六位有效数字,禁止四舍五入或截断,将一种国家货币转换为另一种国家货币只能通过欧元进行。外加最后的附带条件:任何其他计算方法只有在产生相同结果时才被允许。

转换汇率应采用六位有效数字。
转换时不得对转换汇率进行四舍五入或截断。
不得使用从转换汇率导出的逆汇率。
要从一种国家货币单位转换为另一种国家货币单位的货币金额,应首先转换为以欧元单位表示的货币金额,该金额可以四舍五入到不少于三位小数……除非产生相同结果,否则不得使用替代计算方法。

第 5 条添加了你不会在任何标准中找到的内容:关于恰好一半时的行为规则。应支付的金额四舍五入到最接近的分,如果转换正好落在半途——则向上舍入。

当根据第 4 条转换为欧元单位后进行舍入时,应支付或记账的货币金额应向上或向下舍入到最接近的分。……如果应用转换汇率得到的结果正好是半途,则该金额应向上舍入。

英国税务机关用“半便士”的分数表达了同样的意思。HMRC 要求增值税计算到小数点后三位,并按相同规则舍入:少于半便士则向下舍入,半便士或更多则向上舍入(HMRC VAT Trader Records, VATREC12030):

如果任何交易的增值税低于 0.5 便士,则应向下舍入。如果增值税达到 0.5 便士或更多,则应向上舍入。

因此,在法规中找不到任何直接规定使用精确小数类型的内容。但综合起来,它们构成了一个功能上等效的要求,它由四个部分组成:

  1. 存储必须精确到小额货币单位。 二进制浮点数无法精确表示 0.01,因此构建在 double precision 上的系统从一开始就违反了这一点,无论应用程序代码编写得多么小心。
  2. 舍入是一个在单点执行的受监管操作,而非算术的副作用。 欧盟法规尽可能直白地表述:不得对汇率进行舍入或截断,舍入到分恰好发生一次,即在转换之后。
  3. 恰好一半时的行为被明确指定。 欧盟和 HMRC 都特别指出了 0.5 的情况:向上舍入。一个平局决胜规则是实现定义的(implementation-defined)的类型,本身并不能实现这一规范。
  4. 小数位数取决于货币,而非类型: 欧盟中间转换中有三位小数,HMRC 有半便士分数。因此,小数位数必须是模型的一个参数,而不是固定于模式中的常量。

法律从未指明一个类型——它描述的是行为。在 PostgreSQL 中,满足这组要求的类型是 numeric:精确的十进制存储、无隐式舍入、以及显式控制的小数位数。

而就在这里,出现了一些监管机构未曾规定的内容。法律明确规定的舍入规则在 DBMS 中并未固定——它因系统而异,甚至在同一系统中的不同类型之间也不同。Bill Schneider 剖析了 Spark 和 SQL Server 在完全相同的小数数据上如何分歧:一个系统舍入,另一个系统截断。IBM 在 Db2 中走得比任何人都远,直接将规则变成了一个设置——七种模式可供选择,默认值在安装时选定。换句话说,没有哪个 DBMS 的数据类型能保证你“恰好一半时向上舍入”:舍入必须显式地、在一个地方完成,正如第 2 点所要求的那样。

3. 金融数据交换格式

法律谈的是行为,而非表示。然而,交换格式没有这种奢侈:它们必须确定一种具体的编码,否则两个系统根本无法相互理解。

ISO 20022 是 SWIFT、SEPA 和国家支付系统运行的基础。消息模式位于官方目录中,货币金额在其中定义如下(camt.053):

<xs:simpleType name="ActiveCurrencyAndAmount_SimpleType">
    <xs:restriction base="xs:decimal">
        <xs:totalDigits value="18"/>
        <xs:fractionDigits value="5"/>
    </xs:restriction>
</xs:simpleType>
<xs:complexType name="ActiveCurrencyAndAmount">
    <xs:extension base="ActiveCurrencyAndAmount_SimpleType">
        <xs:attribute name="Ccy" type="ActiveCurrencyCode" use="required"/>
    </xs:extension>
</xs:complexType>

从结构上看,这就是一个 NUMERIC(18,5) 加上一个附加到值本身的强制货币代码:没有 Ccy 的消息验证失败。文本中没有明确禁止浮点数,但意图很明确:在官方的 JSON 绑定中,金额以字符串形式传输,这意味着固定大小是首选选项。(ISO 20022 JSON Schema 草案,2025 年 6 月 10 日):

数字类型表示为字符串,因为优先考虑在模式中验证总位数和小数位数。

FIX(交易所交易)的设置方式很奇特:那里的类型称为 float,但名称背后并非二进制浮点数。在基于文本的 FIX 4.4 中,float 是一串带有可选小数点和符号字符的数字序列——一个用字符拼写出来的十进制数——规范要求它至少能容纳十五位有效数字(FIX 4.4 词典,Onix):

带有可选小数点和符号字符的数字序列……所有 float 字段必须能容纳多达十五位有效数字。

在 FIXML 中,绑定是显式的:floatQtyPriceAmtPercentagexs:decimal

而在交易所用于推送报价流的 SBE 二进制编码中——指令是明确的:对价格和所有货币相关内容使用十进制编码,二进制浮点数仅用于那些不代表价格或货币金额的数字字段。(FIX SBE v1.0 RC4,字段编码):

十进制编码应用于价格和相关的货币数据类型,如 PriceOffset 和 Amt。
二进制浮点编码与 IEEE 浮点运算标准(IEEE 754-2008)兼容。它们应用于不代表价格或货币金额的浮点数字字段。

4. 支付系统:小数位数作为货币的一个属性

支付系统值得单独一看,因为在那里立足的解决方案既不存在于标准中,也不存在于法律中。支付行业根本不将钱作为小数传输。原因很可能在于系统的年代——在于塑造了这些标准的 1960-70 年代的标准和 IT 能力。

ISO 8583,银行卡网络协议,定义了数据元 DE 4(交易金额),格式为 n 12——十二位数字,仅此而已。在一个纯数字的定长字段中,根本没有小数分隔符的位置,因此小数位数来自外部——来自货币代码字段。线路上的金额是一个整数,它携带多少位小数由货币决定。货币在这方面有所不同:日元(JPY)、韩元(KRW)和越南盾(VND)为零,美元(USD)、欧元(EUR)和英镑(GBP)为两位,而巴林第纳尔(BHD)、科威特第纳尔(KWD)、阿曼里亚尔(OMR)、突尼斯第纳尔(TND)和约旦第纳尔(JOD)为三位。

因此,在支付系统中,小数位数是货币的一个属性,而不是数字的属性。它不与值一起存储,也不与值一起传输;它位于一个单独的字段中,数字保持为整数。

支付 HTTP API 继承了卡网络的遗产,而那些后来者——以及来自卡世界之外的人——通常选择将金额表示为十进制字符串:

系统 / 格式金额消息格式
ISO 8583整数二进制
FIX SBE整数JSON
Stripe整数JSON
Adyen整数JSON
Square整数JSON
Klarna整数,最小单位JSON
Google Money整数protobuf(二进制),REST 为 JSON
PayPal小数JSON
Shopify小数JSON(GraphQL)
Mollie小数JSON
Braintree小数JSON
Wise(旧版 v1)小数JSON
Plaid小数(双精度)JSON
ISO 20022小数(decimal)XML
FIX 4.x / 最新小数,文本文本(tag=value)

从这张表中浮现出第三层需求,人们很少想到这一层。争论通常集中在存储上——分是否可表示。有时会扩展到计算——确定性、舍入点、恰好一半时的规则。但还有序列化:值必须能够跨越边界,在那里它会被一个你未编写且无法控制的解析器拆解。

这一层的要求与前两层不同。这里需要的不是系统内部的精确性,而是一致性:不同语言的两种独立实现必须得出完全相同的值,精确到最后一位。而恰好有一种构造能满足这一点——一个精确的整数加上一个在值外部定义的小数位数。并非因为整数在某种程度上更精确,而是因为整数是唯一一种每种语言和每个解析器都毫无例外地达成一致的数字类型。与此同时,小数位数则各行其道:在流模式中(Arrow、Parquet、FIX SBE),在相邻字段中(Google 的 units + nanos),或在货币代码中(ISO 8583、Stripe、Adyen)。

因此,支付实践并不排斥精确小数类型——它在其之上添加了第三个、独立的需求,这个需求既不存在于法律中,也不存在于 SQL 标准中。

5. TPC 基准测试要求什么

基准测试是一种特殊的来源:它们不描述事情应该如何做——它们记录了供应商同意竞争的内容。而这里出现了一个分歧。TPC-C 基准测试要求精确计算并遵守 SQL 标准。

标准规范 Rev. 5.11,第 1.3.1 节:

包含货币值的数字字段(W_YTD、D_YTD、C_CREDIT_LIM、C_BALANCE、C_YTD_PAYMENT、H_AMOUNT、OL_AMOUNT、I_PRICE)必须使用由 DBMS 定义为精确数值数据类型或满足 ANSI SQL 标准定义的精确数值表示的数据类型。

TPC-E 也是如此,只是更严格:那里的货币类型以具体精度声明——余额为 SENUM(12,2),聚合为 SENUM(15,2)——并且实现必须提供所声明小数位的精确表示。

v1.14.0,第 2.2.1 节:

ENUM 和 SENUM……必须使用提供至少 n 位小数精度的精确表示的原生数据类型来实现。
BALANCE_T 定义为 SENUM(12,2)…… FIN_AGG_T 定义为 SENUM(15,2)……

面向分析的基准测试——TPC-H 和 TPC-DS——降低了对精确性的要求。对于 Integer,精确性要求被严格规定;对于 Decimal,则不然:聚合获得 1%(AVG 和比率)和 100 美元以内(SUM)的容差。只有 COUNT 要求精确匹配。

TPC-H v3.0.1,第 1.3.1 节:

Decimal 意味着该列必须能够以 0.01 为增量表示 −9,999,999,999.99 到 +9,999,999,999.99 范围内的值;这些值可以被精确表示或被解释为此范围内的值。

一个有趣的结论:事务性基准测试逐字要求精确类型,而分析性基准测试则明确允许近似。这样的放宽可能为被声明为分析性的查询的各类优化打开了大门。

6. 供应商、权威机构和实践者怎么说

接下来,值得看看那些应该了解情况的人写了什么:DBMS 开发者自己,以及在此类争论中常被引用的作者。

对于供应商来说,情况是统一的。PostgreSQL、SQL Server、MySQL 的文档和 IBM 的材料在一个点上达成一致:在需要精确答案的地方,不应使用二进制浮点数——而财务计算被明确地作为例子提及。但请注意这些文本没有包含什么。没有一个供应商说“使用 numeric”——他们都从反面陈述了要求,将在精确小数类型和整数型美分数之间的选择留作开放。

权威机构的情况完全相同,这也许是我最大的惊讶:他们通常被引用来支持 decimal,而他们实际说的却是别的。

Joshua Bloch 在《Effective Java》(第 3 版,Addison-Wesley;第 60 条:“如果需要精确答案,请避免使用 float 和 double”)确实禁止在需要精确答案的地方使用 float 和 double。但他随后将 BigDecimal 和整数类型放在了平等的位置上:BigDecimal——如果你希望系统跟踪小数点并愿意为此付出不便和性能代价;整数——如果性能至关重要。他划出了一个具体的界限:最多九位数字,int 即可;最多十八位——long

总之,不要将 float 或 double 用于任何需要精确答案的计算。如果你希望系统跟踪小数点并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal……如果性能至关重要……请使用 int 或 long。如果数量不超过九位十进制数字,可以使用 int;如果不超过十八位数字,可以使用 long。

Martin Fowler 在《企业应用架构模式》中将货币值单独作为一个模式,Money——其本质不是类型的选择,而是金额和货币必须一起传输,并且舍入到最小单位必须是显式操作。在表示方面,Fowler 只对一件事是明确的:没有二进制浮点数。在整数和小数之间,他没有做出选择。(该书可购买。)

然而,在 Postgres 社区中,立场存在分歧,两者都值得引用。Cybertec 的 Hans-Jürgen Schönig 是明确的:货币需要特殊的舍入规则,这就是为什么财务数据应使用 numeric

就货币而言,需要不同的舍入规则,这就是为什么你必须使用 numeric 数据类型来处理财务数据。

Crunchy Data 的 Elizabeth Christensen 给出了一个带分支的推荐,更接近我们看到的 Bloch 的观点:“整数——如果整分适合你且你不需要分数分;numeric——如果你需要分的分数和总体上很多位数字。以及作为一个单独的点:将货币存储在附近,但在其自己的字段中。”总结:

如果你可以使用整数的分并且不需要分数分,请使用 int 或 bigint。
如果需要存储分数分甚至很多位小数点的货币,请使用 decimal/numeric。
将货币与实际货币值分开存储……

反对整数分的最详尽论点来自 Otar Chekurishvili,它不关乎精确性,而是关乎错误存在于何处。通过存储 1999 而不是 19.99,你并没有解决问题——你只是把它移出了数据库,移到了应用程序的每一层:每个读取该列的人现在都必须了解隐式的小数位数。

当你存储 1999 而不是 19.99 时,你实际上并没有解决问题。你只是把它移出了数据库,移到了应用程序的每一层。

同样说明问题的是 Java 货币规范(JSR 354)所做的:它故意不固定表示,因为对其的要求因使用场景而异:

JSR 354 明确支持实现和使用不同类型的货币金额。
原因在于,对不同使用场景而言,对实现的要求差异很大。

这并非空泛的警告:参考实现同时提供了两个类——基于 BigDecimalMoney 和基于 longFastMoney。后者的 javadoc 直截了当地说明,它的速度快 10-15 倍,代价是精度有限。

7. ERP 系统中发生了什么

剩下的就是看看那些每天处理金钱的应用系统——同时,检查声明的类型是否与实际对数字发生的情况相匹配。

  • SAP——打包小数(packed decimal)加一个强制货币字段。ABAP 文档,货币字段。
  • Oracle E-Business Suite / Fusion——NUMBER,完全没有精度或小数位数。GL_JE_LINES。
  • Odoo——最有趣的案例;我们稍后会单独回到它。数据库列的类型是 numeric(odoo/fields.py 17.0),而 ORM 中的值是 Python float;因此有了整个辅助模块,odoo/tools/float_utils.py
  • 1C:Enterprise(主导俄罗斯和独联体国家的 ERP 平台)。该平台的“Number”类型——ITS,“表达式结果和聚合函数的精度”。最大精度 38 位,记法形式为 Число(17,4)——即 Number(17,4)——以及加法、乘法和除法结果精度的派生规则。

Odoo 值得单独说明,因为结果出乎意料。我们看到的是一个生产级 ERP,其列被声明为 numeric,而所有算术运算都在二进制双精度浮点数中运行——以及由此带来的一切后果,这正是 float_utils.py 及其比较和舍入辅助函数必须被编写出来的原因。教训比任何单一供应商都更广泛:模式中的列类型本身,并不意味着计算会以十进制算术运行。模式保证的是存储。如果应用程序将值拉入 double,进行了计算,然后写回,精度在应用层就丢失了——而数据库看起来是完美的。

本节的总体结论,即对本文所问问题重要的结论是:在支付系统中,正如我们所看到的,小数位数是货币的一个属性:只有一个,它来自外部。在会计系统中,一切都不同。在 1C 中,精度是按属性设置的,带有 NUMBER(N,M) 记法:金额通常带两位小数,数量带三位,系数和汇率更多。在 SAP 中,货币字段必须与货币代码字段配对,然而小数位数是由数量的种类决定的,而不仅仅由货币决定。换句话说,这里的小数位数属于领域,而非货币单位——并且单个数据库中同时存在多个这样的领域。这正是整数型美分数无法拯救 ERP 的原因:每个模式一个小数位数常数是不够的,而 SQL 将小数位数固定在列类型中,而非值中。

8. 精确小数类型的发展方向

现在谈谈趋势。精确小数类型不会消失——恰恰相反,它正在不断扩展。

  • 自 2025 年底以来,国际支付运行在 ISO 20022 上——其中货币金额的精度为 totalDigits="18",即小于 int64 所能提供的。承载全球银行间结算的格式不需要任意精度。
  • C23 将 Decimal32/64/128 引入了 C 语言标准(cppreference,C23)——虽然是可选的,位于 _STDC_IEC_60559_DFP__ 宏之后。GCC 部分支持;Clang 和 MSVC 尚未支持。(LLVM 中的 RFC 仍然开放。)
  • SQL:2016 中的 DECFLOAT 不仅在 Db2 中实现,在 Firebird 4.0 中也实现了(README:floating_point_types)。

精确小数类型存在于众多 DBMS 和每种列式格式中。

然而,在分析引擎中,情况是混合的——这很好地显示了精确性在哪些地方被认为是强制性的,在哪些地方是可协商的。Apache Druid 根本没有精确数值类型,并拒绝了一项添加它的提案。ClickHouse 能很好地使用 Float64,只是建议在需要精确性时使用 Decimal。而 Elasticsearch 和 Power BI 走了第三条路——在整数之上进行固定小数位的精确算术。也就是说,分析引擎达成的共识不是“一种精确类型”,而是“在固定小数位上进行精确加法”——这正是 TPC-H 和 TPC-DS 所允许的放宽。

同样的分歧延伸到了 DBMS 之外。Bloomberg 的市场数据 API——来自一家整个业务都是金融数据的公司——将价格作为 FLOAT64 即二进制浮点数发送,而其 DECIMAL 数据类型至今标记为“Currently Unsupported”。毕竟,报价不是账本条目:对于流式市场数据,宽松的、在 1% 以内的世界就足够了——这正是分析引擎所画的线。

38 位数字的天花板
不再流行的是变长任意精度。38 位数字(int128)的天花板看起来像一个事实上的通用常数:

系统上限表示
Snowflake38基于实际范围的适应性宽度
Redshift38最高 19 位用 int64,最高 38 位用 int128
SQL Server / Synapse385/9/13/17 字节
Databricks / Spark38最高 18 位用 long 快速路径,超出用 BigDecimal
DuckDB38INT16/32/64/128
Iceberg38精度必须为 38 或更小
ClickHouse76int32/64/128/256
BigQuery38 / ~76.8具有不同小数位数的 int128
YDB35int128, 16 字节,精度和小数位数在类型中

固定宽度的代价
每个工程决策都意味着一些权衡,而将其精确小数类型设上限的 DBMS 也不例外。例如,Redshift 明确劝告用户不要“以防万一”地抓取最大精度:128 位的值占用空间是 64 位值的两倍,并会减慢查询执行速度(文档):

除非你确信你的应用程序需要该精度,否则不要任意将最大精度分配给 DECIMAL 列。128 位的值使用的磁盘空间是 64 位值的两倍,并可能减慢查询执行时间。

Apache Arrow 精确固定了四种宽度,并直接描述了表示:精确小数值存储为二进制补码整数——32、64、128 或 256 位——而小数位数存在于流模式(Schema.fbs)中:

表示为二进制补码整数值的精确小数值。目前使用 32 位(4 字节)、64 位(8 字节)、128 位(16 字节)和 256 位(32 字节)整数。
接受的宽度为 32、64、128 和 256。

此外,演进方向是向更窄而非更宽:Arrow 18.0.0(2024 年 10 月)添加了 Decimal32 和 Decimal64,而非更宽的类型:

这一点很容易理解——性能:在当今的硬件上,32 位和 64 位操作远比 128 位操作便宜。

最说明问题的来源是 CedarDB——Umbra 的商业继承者,一个在 2020 年代设计的引擎。它明确地、书面地将自己与 PostgreSQL 对立起来:PostgreSQL 提供高达 131072 位的精度,CedarDB 将其上限设为 38——并以明文说明这样做是为了性能。同时,它建议保持在 18 位以内,因为对 16 字节值的操作很昂贵,并且它禁止 PostgreSQL 允许的 NaN 和无穷大(numeric 文档):

PostgreSQL 提供最大精度 131072 和小数位数 16383,而 CedarDB 出于性能原因将精度和小数位数限制为最大 38。
我们建议在可能的情况下为你的应用程序使用 18 或更低的精度。
PostgreSQL 允许 NaN、+Infinity 和 -Infinity 作为特殊的数值。
CedarDB 禁止将这些值作为数值数据类型输入。

一个专门构建快速 PostgreSQL 兼容引擎的团队,恰恰去掉了使 numeric 变慢的东西:任意精度、varlena(Postgres 内部变长属性)和特殊值。

自然,一切都有代价。例如,ClickHouse 诚实地记录其宽固定类型完全没有溢出检查。也就是说,使用 Decimal32/Decimal64 时,整数部分的溢出会引发异常,而使用 Decimal128/Decimal256 时,你会悄悄地得到错误结果。

由于固定宽度类型有有限的数字预算,设计者必须达成平衡——在这里,是在范围宽度、溢出检查和特殊值之间。下表列出了我设法识别的权衡:

引擎付出的代价
ClickHouse溢出检查——Decimal128/Decimal256 没有
CedarDB特殊值——NaN 和 ±Infinity 被禁止
YDB范围——38 位中的三位十进制数字预留给哨兵值
PostgreSQL numeric

同样说明问题的是,YDB 是独立设计的——与 CedarDB 和 DuckDB 处于相同的 2010 年代至 2020 年代——并且得出了相同的构造:固定宽度整数,小数位数在类型中,上限约为 35–38。变长任意精度未被任何人选择。

结论

那么,这次小小的调查给了我们什么?

首先,realdouble precision(实际进行算术运算的数量)的用途范围正在缩小至零。

然而,在整数和精确小数之间,存在着一个真正的选择。区别它们的是小数位数定义在哪里以及舍入如何执行。如果小数位数来自外部,并且对每一行都相同——来自货币代码,来自协议常量——并且舍入规则不重要,那么整数工作得很好。但如果小数位数由领域设置,因字段而异,并且在实时系统中被重新配置,那么整数在原则上就不合适,因为整个数据库的一个小数位数常数是不够的。Otar Chekurishvili 在“将货币存储为整数美分数通常是过度工程”中说得非常精确:整数并不能解决问题——它只是将问题转移到了应用程序上。

第三个结论结果相当出乎意料。通常的争论是关于存储——如何表示分。有时会扩展到计算——确定性、舍入点、恰好一半时的规则。但有第三个方面——序列化:值必须能够跨越边界,在那里它会被一个你未编写且无法控制的解析器拆解。

同时,精确小数类型的性能显然在引擎开发者的议程上,而常见的答案看起来都一样——限制宽度。典型的上限设定为 38 位。然而,这并不总是能容纳于 int128 中,因此必须找到妥协:一些放宽溢出检查,一些放弃像 NaN 或 Infinity 这样的特殊值,一些放弃部分范围。没有人选择变长任意精度——除了 PostgreSQL。

对我来说,主要的 Postgres 见解是,PostgreSQL 的内置类型系统可能缺少一个具有界限精度和小数位数的 numeric。这正是 Arrow、SQL Server、DuckDB、ClickHouse、YDB 和 Power BI 所选择的构造——也正是我们所没有的。是的,历史知道至少三次构建“快速”小数作为扩展的失败尝试。但根据其他人的发展方向来看,这种需求将变得越难忽视。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐