跳到主要内容

货币类型

金额不是“带小数的普通数字”这么简单。金额字段还涉及货币单位、小数位、舍入、汇率和展示格式。PostgreSQL 提供 money 类型,但在多数业务表中,numeric(p, s) 更容易跨环境控制精度;money 适合已经接受本地化显示和固定小数位语义的场景。

money 的特点​

money 保存带有货币格式的定点数。它的显示符号和小数位受数据库的 lc_monetary 区域设置影响:

SHOW lc_monetary;

SELECT 1234.56::money;

同一个值在不同服务器或客户端上可能显示为不同的货币符号和分隔符。展示结果不应作为接口的稳定格式。

money 的存储范围依 PostgreSQL 版本和平台实现而定,通常以 64 位整数表示最小货币单位。它不保存 ISO 4217 货币代码,因此一个列不能同时表达“金额”和“币种”。

输入、输出与转换​

可以从字符串或数值转换为 money,但字符串解析会受区域设置影响:

SELECT '¥1,234.50'::money;
SELECT 1234.50::numeric::money;

为了避免区域设置导致导入失败,批量导入时优先使用数值或 numeric 中间列,再显式转换。money 转为整数或 numeric 时也应明确目标精度:

SELECT amount::numeric(12, 2)
FROM payments;

money 与整数相除的结果通常仍是 money,与 numeric 相除则取决于运算符定义。金额计算前先查看表达式类型,必要时显式转换为 numeric。

为什么通常选 numeric​

CREATE TABLE invoices (
invoice_id bigint PRIMARY KEY,
currency_code char(3) NOT NULL,
total_amount numeric(18, 2) NOT NULL,
CHECK (total_amount >= 0)
);

numeric 的优点是:

  • precision 和 scale 可直接写进表结构,便于表达业务上限;
  • 不依赖服务器的 lc_monetary,导入、复制和 API 序列化更可预测;
  • 可与利率、税率等其他精确数值统一计算;
  • 迁移到其他数据库时通常比 money 更容易。

如果一个系统支持多币种,至少要同时保存金额和货币代码。货币代码可以使用 char(3),也可以建立受约束的字典表:

CREATE TABLE currencies (
code char(3) PRIMARY KEY,
minor_units smallint NOT NULL CHECK (minor_units BETWEEN 0 AND 4)
);

舍入与最小货币单位​

不要把“显示两位小数”误当作舍入规则。结算时应明确:税费在行项目还是订单级计算、何时四舍五入、分摊余数给哪一行。PostgreSQL 可以显式调用 round:

SELECT round((unit_price * quantity)::numeric, 2)
FROM invoice_items;

另一种方案是用整数保存最小货币单位,例如分:

amount_minor bigint NOT NULL CHECK (amount_minor >= 0),
currency_code char(3) NOT NULL

整数方案避免小数位不一致,但必须在业务层知道每种货币的最小单位,不能假设所有货币都是两位小数。

索引、聚合与接口​

money 和 numeric 都可以建立 B-tree 索引并参与排序、聚合。金额过滤应使用参数化查询,避免把带本地化符号的文本拼入 SQL:

SELECT invoice_id, total_amount
FROM invoices
WHERE total_amount >= $1::numeric(18, 2);

向 JSON、CSV 或前端输出时,通常输出不带本地化符号的十进制字符串和独立的 currency_code。不要依赖 money 的默认文本表示作为机器接口协议。

常见误区​

  • 用浮点类型保存金额,导致 0.1 + 0.2 一类的二进制表示误差。
  • 只存金额不存币种,无法区分美元、人民币和日元。
  • 在不同区域设置的服务器之间直接导入 money 文本。
  • 只给最终结果设置精度,却没有为中间乘法、税率计算预留位数。
  • 把负金额、退款和冲正简单禁止;是否允许负数应由业务状态决定。

选择清单​

  1. 需要跨数据库、稳定序列化和明确精度时,优先使用 numeric(p, s)。
  2. 已经依赖 PostgreSQL 区域货币显示且范围固定时,才考虑 money。
  3. 多币种一定保存货币代码,并维护小数位或最小货币单位规则。
  4. 计算规则(精度、舍入、分摊)写入代码、约束和测试,而不是交给默认格式。