ES 不支持 decimal,如何避免丢失精度?
结论先行:ES 没有精确十进制类型,只有
double、float、half_float、scaled_float。金额场景的标准做法是用scaled_float配合scaling_factor(如 100),把两位小数缩放成整数存储;需要绝对精确与对账时,再用keyword存原始字符串。double只适合统计近似值,绝不能用于账务。
一、三种方案的精度对比
| 方案 | 存储 | 精度 | 可范围查询 | 可聚合 | 适用 |
|---|---|---|---|---|---|
double | IEEE754 双精度 | 二进制浮点,存在舍入误差 | 是 | 是 | 指标、均值、监控 |
scaled_float | 缩放后按 long 存 | 定点精确(到因子位) | 是 | 是 | 金额、单价、税率 |
keyword | 原始字符串 | 完全精确 | 否(字典序) | 否 | 展示、对账、导出 |
0.1 + 0.2 在 double 里等于 0.30000000000000004
把 12.34 乘 100 存成 1234(long),读取时再除 100 —— 全程整数,无误差
二、scaled_float 的映射与读写
PUT /order
{
"mappings": {
"properties": {
"order_id": { "type": "keyword" },
"amount": { "type": "scaled_float", "scaling_factor": 100 },
"currency": { "type": "keyword" }
}
}
}
写入时直接给原值,ES 内部负责缩放:
POST /order/_doc/1
{ "order_id": "A1001", "amount": 12.34, "currency": "CNY" }
查询与聚合:
GET /order/_search
{
"query": { "range": { "amount": { "gte": 10.00, "lte": 20.00 } } },
"aggs": {
"total": { "sum": { "field": "amount" } }
}
}
返回的 sum 是缩放后的整数再转回小数,可能出现 1234.0000000000002 这类展示噪声,展示层要格式化到两位。
三、必须自己兜住的三个边界
| 边界 | 问题 | 处理 |
|---|---|---|
| 因子选小了 | 12.345 被截断成 12.34 | 按业务最大小数位选因子,如三位小数用 1000 |
| 因子选大了 | 数值超出 long 范围 | 金额上限 × 因子必须小于 9.2e18 |
| 负数与四舍五入 | 缩放时截断方向不一致 | 写入前在应用层统一 BigDecimal.setScale(2, HALF_UP) |
// 应用层统一精度,再交给 ES
BigDecimal amount = raw.setScale(2, RoundingMode.HALF_UP);
doc.put("amount", amount.doubleValue()); // scaled_float 接收 double 入参
doc.put("amount_str", amount.toPlainString());
四、double 到底差多少
写入 0.29,读回可能是 0.28999999999999998
10 万笔 0.1 元求和,double 聚合结果可能是 9999.999999998
→ 用于报表趋势没问题,用于财务对账就是事故
五、选型决策
| 场景 | 方案 |
|---|---|
| 订单金额检索与区间筛选 | scaled_float(factor=100) |
| 财务对账、发票金额 | keyword 存 BigDecimal.toPlainString() |
| 同时要检索与对账 | 双字段:amount(scaled_float)+ amount_str(keyword) |
| 比率、评分、监控指标 | double 可接受 |
常见追问
- 追问:为什么不用
long存“分”?可以,scaled_float本质就是这么做的,但long需要业务层自己缩放,scaled_float由 ES 统一处理查询与聚合。 - 追问:
scaled_float的聚合一定精确吗?单值精确,但sum/avg在内部仍用 double 计算,超大额累加仍可能有浮点误差,关键场景要在应用层用BigDecimal复算。 - 追问:MySQL 的
decimal同步到 ES 怎么做?binlog 里读出来是BigDecimal,写入前统一setScale并同时写字符串字段,避免精度在管道里丢失。
小结
- ES 无 decimal,
double有二进制舍入误差,不能用于金额。 scaled_float+scaling_factor是金额检索的标准解。- 因子要匹配业务小数位,写入前用
BigDecimal统一舍入。 - 对账场景再加一个
keyword字符串字段,双保险。