ES 不支持 decimal,如何避免丢失精度?

#中间件#面试八股

ES 不支持 decimal,如何避免丢失精度?

结论先行:ES 没有精确十进制类型,只有 double、float、half_float、scaled_float。金额场景的标准做法是用 scaled_float 配合 scaling_factor(如 100),把两位小数缩放成整数存储;需要绝对精确与对账时,再用 keyword 存原始字符串。double 只适合统计近似值,绝不能用于账务。

一、三种方案的精度对比

方案存储精度可范围查询可聚合适用
doubleIEEE754 双精度二进制浮点,存在舍入误差是是指标、均值、监控
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 字符串字段,双保险。
笔记加载中…