数据库金额字段应该如何设计?为什么不建议用 float?
简化版
金额字段不要用 float 或 double,因为二进制浮点数无法精确表示很多十进制小数。常见设计是用 DECIMAL(p,s) 保存金额,或用整数保存最小货币单位,同时配套币种、精度、舍入规则和流水记录。
详细版
金额设计要关注精度、范围、币种和审计。普通业务可以用 DECIMAL(18,2) 保存人民币金额;高并发账务、支付清结算系统常用整数分保存,避免小数计算误差。
常见原则:
- 不用
float/double存钱。 - 明确单位:元、分、厘不能混。
- 涉及多币种时要有
currency字段。 - 计算结果要明确舍入规则。
- 账户余额类数据必须有资金流水,不应只相信余额字段。
例如订单金额可以是 amount decimal(18,2);支付系统内部可以用 amount_cent bigint。前者可读,后者计算确定性更强。
完整版教学
一、金额字段的核心问题是十进制精度
金额看起来只是一个数字,但它和普通计数不同。钱的计算要求确定、可审计、可复现,不能出现“多一分钱少一分钱”的随机误差。
float 和 double 是二进制浮点数,很多十进制小数无法被精确表示。比如 0.1 在二进制里是无限循环,计算后可能得到类似 0.30000000000000004 的结果。
0.1 + 0.2 != 0.3 // 浮点表达下可能成立
记忆钩子:金额不是“能算就行”,而是“每一位都要能解释”。所以面试里先排除 float/double。
二、DECIMAL 和整数分是两条主流路线
数据库里金额常见两种保存方式:DECIMAL(p,s) 和整数最小单位。DECIMAL(18,2) 表示最多 18 位数字,其中 2 位小数,适合保存常规金额。
整数分方案则把 12.34 元保存为 1234 分。这样加减乘除时本质是整数运算,尤其适合支付、账户、清结算等对一致性要求很高的场景。
| 方案 | 示例 | 优点 | 代价 |
|---|---|---|---|
DECIMAL(18,2) | 12.34 | 可读性好,SQL 展示方便 | 计算要注意精度和舍入 |
BIGINT 分 | 1234 | 整数确定性强,计算简单 | 展示时要转换单位 |
FLOAT | 12.34 | 存储和计算快 | 不适合金额精确计算 |
普通订单表用 DECIMAL 完全可以,支付核心链路更常见整数分。
三、p 和 s 不能随便拍脑袋
DECIMAL(p,s) 里的 p 是总位数,s 是小数位数。比如 DECIMAL(10,2) 最大能保存 99999999.99,也就是整数部分 8 位。
如果业务未来可能出现大额订单、企业账户、跨境结算,DECIMAL(10,2) 可能不够。很多系统会选择 DECIMAL(18,2) 或 DECIMAL(20,6),既留范围也支持更细精度。
DECIMAL(10,2): 99,999,999.99
DECIMAL(18,2): 9,999,999,999,999,999.99
DECIMAL(20,6): 99,999,999,999,999.999999
字段范围不是越大越好,但要能覆盖业务上限。面试回答可以说明:先根据最大交易额、累计金额和币种小数位确定范围,而不是直接套模板。
四、多币种必须保存币种和精度语义
如果系统只处理人民币,金额字段可以默认两位小数。但一旦有美元、日元、港币、积分、虚拟币,就不能只存一个 amount。
日元通常没有小数,很多虚拟资产可能有 6 位或 8 位小数。没有 currency 字段,后续展示、换汇、对账都会失去上下文。
推荐结构类似:
amount decimal(20,6) not null,
currency varchar(8) not null,
rate_source varchar(32) null,
created_at timestamp not null
如果用整数最小单位,也要明确这个整数对应哪种币种的最小单位,否则 100 到底是 100 分、100 日元还是 100 个积分会变得含混。
五、舍入规则要固定,不能散落在各处
金额乘折扣、税率、汇率时会遇到舍入。比如 19.99 * 0.85 = 16.9915,最终收 16.99 还是 17.00,必须有规则。
常见舍入策略包括四舍五入、向上取整、向下取整、银行家舍入。支付、税务、清结算系统尤其要把规则写进代码、文档和测试里。
19.99 * 0.85 = 16.9915
保留 2 位:
四舍五入 -> 16.99
向上取整 -> 17.00
同一个系统不能 A 接口向下取整,B 任务四舍五入。金额差异小,但账务对不上时排查成本很高。
六、余额字段必须配流水,不能只改余额
账户余额是结果,资金流水是原因。只保存 balance,后续无法解释余额为什么变化,也无法做审计和对账。
比较稳的设计是账户表保存当前余额,流水表记录每一笔入账、出账、冻结、解冻。更新余额和插入流水要放在同一个事务里。
account(id, balance_cent, frozen_cent, updated_at)
account_flow(id, account_id, direction, amount_cent, balance_after, biz_no, created_at)
如果一笔扣款是 100 分,流水里记录扣款前后余额,就能在排障时复盘。余额字段可以提升查询效率,但不能替代流水。
七、常见误区与追问
- 误区:金额用 double 更快所以更好。 金额场景优先正确性和可审计,浮点误差会破坏对账。
- 误区:所有金额都用 DECIMAL(10,2)。 不同业务范围不同,大额累计、汇率、虚拟资产可能需要更大范围或更多小数位。
- 误区:只存 amount 不存 currency。 多币种或积分场景会丢失单位语义,后续展示和对账都不可靠。
- 追问:为什么整数分更适合支付核心链路? 因为加减是整数运算,结果确定,序列化和跨语言处理更稳定。
- 追问:余额表和流水表是什么关系? 余额是当前快照,流水是变更明细;余额便于查询,流水负责审计和对账。
- 追问:金额字段要不要建索引? 只有按金额范围筛选、排序、风控检索时才考虑,普通展示字段不必单独建索引。
八、面试中可以这样给方案
如果题目问“订单金额怎么设计”,可以回答:订单表用 DECIMAL(18,2) 或整数分,明确币种,优惠、税费、实付金额分字段保存,避免每次临时反算。
create table orders (
id bigint primary key,
total_amount decimal(18,2) not null,
discount_amount decimal(18,2) not null default 0,
pay_amount decimal(18,2) not null,
currency varchar(8) not null default 'CNY',
created_at timestamp not null
);
如果是账户余额,就强调事务、流水、幂等号和对账。这样能区分普通业务金额和账务核心金额,答案会更像工程经验。
九、加强记忆
金额字段记住四件事:不用浮点数,明确单位和币种,固定精度和舍入规则,余额必须配流水。普通订单偏展示和查询,可以用 DECIMAL;支付账务偏一致性和审计,更适合整数最小单位加流水表。面试时把“精度、范围、单位、审计”串起来讲,就能覆盖大部分金额设计追问。