Access 对 Double 类型列求和时结果不准确
结论
这是典型的二进制浮点精度问题,并不是 Access 求和出错。
Double 无法精确表示大多数十进制金额。把字段格式设置为 Standard 并显示两位小数,只会改变显示效果,不会将底层数值限制为两位小数。因此:
1.13686837721616E-13是一个非常接近0的微小误差。-2.99999999999727E-02约等于-0.03。- 使用
<>精确比较时,即使两个值都显示为728.57,底层数值也可能存在细微差异。
付款金额最好使用 Currency。也可以先在查询中转换成 Currency,再进行比较。如果无法修改数据类型,就应设置合理的允许误差,而不是直接使用 <>。
为什么显示相同,比较结果却不同
Double 使用 IEEE 754 二进制浮点格式。0.1、455.42、728.57 这类十进制小数通常无法用有限位二进制数精确保存,系统只能存储与其非常接近的近似值。
例如,Access 内部的两个值可能类似于:
Amount = 728.57000000000005
Total = 728.56999999999994
格式化为两位小数后,两者都会显示为:
728.57
但执行精确比较:
Total <> Amount
结果仍然是 True。
同样,科学计数法表示的:
1.13686837721616E-13
实际是:
0.000000000000113686837721616
如果金额以“分”为最小单位,这个差值可以视为零。
推荐方案:使用 Currency 类型
Access 的 Currency 是定点数类型,可保留四位小数,适合存储和计算货币金额,也不会产生 Double 的二进制小数误差。
如果可以修改 SharePoint 列定义,应将相关金额列设为货币类型,并确认链接到 Access 后,字段被识别为 Currency。只把显示格式改成两位小数并不能解决问题。
如果链接表中的字段仍是 Double,可以在查询中使用 CCur() 转换。由于参与加法的任意字段为 Null 时,整个结果都会变成 Null,建议同时使用 Nz():
TotalAmount:
CCur(Nz([Regular Payment], 0))
+ CCur(Nz([Principle], 0))
+ CCur(Nz([Interest Only], 0))
+ CCur(Nz([Annual Fee], 0))
+ CCur(Nz([Late Fee], 0))
+ CCur(Nz([Escrow], 0))
+ CCur(Nz([CPI], 0))
+ CCur(Nz([Payoff], 0))
+ CCur(Nz([Unapplied], 0))
+ CCur(Nz([Short Payoff], 0))
比较时,也要转换 Amount:
WHERE
CCur(Nz([Regular Payment], 0))
+ CCur(Nz([Principle], 0))
+ CCur(Nz([Interest Only], 0))
+ CCur(Nz([Annual Fee], 0))
+ CCur(Nz([Late Fee], 0))
+ CCur(Nz([Escrow], 0))
+ CCur(Nz([CPI], 0))
+ CCur(Nz([Payoff], 0))
+ CCur(Nz([Unapplied], 0))
+ CCur(Nz([Short Payoff], 0))
<> CCur(Nz([Amount], 0));
这种写法要求金额不能超出 Currency 的可表示范围,而且业务所需精度最多为四位小数。
无法改变字段类型时:使用允许误差
如果数据必须保留为 Double,就不要判断两个值是否完全相等,而应检查差值是否超过允许范围。
金额精确到“分”时,通常可以用半分钱 0.005 作为边界:
WHERE Abs(
Nz([Amount], 0)
- (
Nz([Regular Payment], 0)
+ Nz([Principle], 0)
+ Nz([Interest Only], 0)
+ Nz([Annual Fee], 0)
+ Nz([Late Fee], 0)
+ Nz([Escrow], 0)
+ Nz([CPI], 0)
+ Nz([Payoff], 0)
+ Nz([Unapplied], 0)
+ Nz([Short Payoff], 0)
)
) >= 0.005;
使用这个条件后:
1.13686837721616E-13不会被判定为不匹配。-1.13686837721616E-13不会被判定为不匹配。- 约为
-0.03的差值仍会被识别出来。
如果业务规则是忽略所有不足一分钱的差异,也可以设置更小的容差,例如:
WHERE Abs([Amount] - [Total]) > 0.000001
具体阈值应根据金额精度和业务规则确定,不能只看显示的小数位数。
也可以按两位小数比较
如果业务明确规定所有金额都先舍入到“分”,可以比较舍入后的结果:
WHERE Round(Nz([Amount], 0), 2) <>
Round(
Nz([Regular Payment], 0)
+ Nz([Principle], 0)
+ Nz([Interest Only], 0)
+ Nz([Annual Fee], 0)
+ Nz([Late Fee], 0)
+ Nz([Escrow], 0)
+ Nz([CPI], 0)
+ Nz([Payoff], 0)
+ Nz([Unapplied], 0)
+ Nz([Short Payoff], 0),
2
);
Access/VBA 的 Round() 使用银行家舍入,中间值可能会舍入到最近的偶数。某些正好位于 .005 边界的值,可能不符合财务系统常用的“四舍五入”规则。更稳妥的长期方案,是从数据录入阶段开始使用 Currency,并统一金额的舍入规则。
注意事项
Decimal Places = 2只控制显示,不会改变字段中实际保存的值。Format()返回的是格式化文本,不适合用于修复或比较金额。- 加法表达式中只要有一个字段为
Null,整个结果就会变成Null,因此应根据业务含义使用Nz(..., 0)。 - 付款、余额和费用等金额应优先使用
Currency。Double更适合科学计算和测量值。 -2.99999999999727E-02并非异常结果,只是-0.03的浮点近似表示。显示时可以将其格式化为两位小数,但比较金额是否匹配时,应使用Currency或允许误差。
备注:内容仅供参考。
