内容提要
在PostgreSQL中存储金额,推荐使用numeric(14,2)等显式精度类型,精确且可直接用于报表;若金额仅累加并往返支付系统,可用bigint整数分,更快但有上限且SQL除法截断。避免real/double和money类型。作者自家记账产品用numeric多精度,计费层用整数分。可用查询审计现有schema。
延伸解读
小数位需求决定类型选择
文章指出,当金额涉及单价、税率等需要小数位时,整数分需要额外约定,而numeric可直接支持。例如单价€0.0125或税率21.000,用整数分存储需第二套规则,增加复杂度。因此,若业务中频繁出现小数金额,numeric(12,4)等显式精度类型更合适,避免后续维护混乱。
numeric的隐式舍入与特殊值风险
numeric列在插入时会按指定标度自动舍入,如2.345存入numeric(12,2)变为2.35,且无警告。此外,未指定精度的numeric可存储任意小数,甚至接受NaN。文章建议为金额列明确标度,并添加CHECK约束排除NaN,防止意外数据进入生产环境。
整数分的上限与除法截断
integer类型以分存储时最大仅约2147万欧元,超出会报错;bigint虽大,但node-postgres默认返回字符串,直接转Number可能丢失精度。SQL整数除法向零截断,如1000/3得333,分摊金额时可能每行丢失一分。因此,整数分适合仅累加且不涉及除法的场景,如订阅计费。
money类型的局限与审计查询
money类型的小数精度和格式依赖数据库lc_monetary设置,且不支持分以下单位,除法结果可能为double precision。文章提供查询帮助审计现有schema,识别real/double precision、无标度numeric及疑似金额的integer列。其中浮点类型存储金额是紧急问题,应优先处理。
Q&A
PostgreSQL 中存储金额应该用哪种数据类型?
推荐使用 numeric 并指定精度和小数位,例如 numeric(14,2) 用于金额,更多小数位用于单价和费率。如果金额仅用于累加并在应用代码和支付系统之间传递,可以使用 bigint 存储整数分。避免使用 real/double precision 和 money 类型。
为什么 PostgreSQL 的 money 类型不推荐用于存储金额?
money 类型无法存储小于一分的小数,其小数精度和格式化依赖于数据库的 lc_monetary 设置,可能导致转储到不同设置的数据库时出错。除以整数会截断,除以 money 会得到 double precision。PostgreSQL wiki 明确建议不要使用。
使用 numeric 存储金额时需要注意哪些坑?
numeric 列如果指定了小数位,插入时会自动四舍五入而不报错;不指定精度和小数位会接受任意小数;可以存储 NaN 值,需要 CHECK 约束排除;除法仍会产生无限小数,需要应用层处理舍入。
整数分(integer cents)存储金额有什么限制?
integer 最大只能表示约 2147 万欧元,超出会报错;bigint 虽然范围大,但超过 JavaScript 安全整数范围后 node-postgres 会返回字符串;SQL 中的除法会向零截断,可能丢失分;无法直接表示小于一分的小数单价。
如何检查现有 PostgreSQL 数据库中金额列的类型是否合理?
可以运行查询检查 information_schema.columns,找出 real、double precision、money 类型的列,没有指定小数位的 numeric 列,以及名称暗示金额的 integer 列。根据结果决定是否需要调整类型。
在 SaaS 产品中,金额存储类型的选择原则是什么?
类型取决于金额的用途:需要乘法、拆分和不同精度报告时使用 numeric 并指定小数位;仅用于计数和传递时使用整数分。作者的产品中,记账部分用 numeric,计费层用整数分。