← 返回题目列表

数据库金额字段应该如何设计?为什么不建议用 float?

高频 中等 第 12 / 33 题 更新于 2026/07/29
数据库设计金额字段DECIMAL精度

简化版

金额字段不要用 floatdouble,因为二进制浮点数无法精确表示很多十进制小数。常见设计是用 DECIMAL(p,s) 保存金额,或用整数保存最小货币单位,同时配套币种、精度、舍入规则和流水记录。

详细版

金额设计要关注精度、范围、币种和审计。普通业务可以用 DECIMAL(18,2) 保存人民币金额;高并发账务、支付清结算系统常用整数分保存,避免小数计算误差。

常见原则:

  • 不用 float/double 存钱。
  • 明确单位:元、分、厘不能混。
  • 涉及多币种时要有 currency 字段。
  • 计算结果要明确舍入规则。
  • 账户余额类数据必须有资金流水,不应只相信余额字段。

例如订单金额可以是 amount decimal(18,2);支付系统内部可以用 amount_cent bigint。前者可读,后者计算确定性更强。

完整版教学

一、金额字段的核心问题是十进制精度

金额看起来只是一个数字,但它和普通计数不同。钱的计算要求确定、可审计、可复现,不能出现“多一分钱少一分钱”的随机误差。

floatdouble 是二进制浮点数,很多十进制小数无法被精确表示。比如 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 展示方便计算要注意精度和舍入
BIGINT1234整数确定性强,计算简单展示时要转换单位
FLOAT12.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;支付账务偏一致性和审计,更适合整数最小单位加流水表。面试时把“精度、范围、单位、审计”串起来讲,就能覆盖大部分金额设计追问。