当前位置:首页 > 问答 > 正文

用SQL Server做那些精准计算,运算细节其实没那么简单

(引用来源:知乎专栏“数据手艺人”、MSDN SQL Server 官方文档、以及资深DBA社区讨论中的常见问题汇总)

直接用SQL Server做计算,很多人觉得不就是写个加减乘除或者用几个内置函数嘛,但真要把结果算得一丝不差,尤其是在处理钱、百分比或者要求高性能的时候,里头门道可就深了,一个不留神,可能对了一年的数据,最后对总账时发现差了几分钱,或者报表上的百分比加起来不是100%,这种问题找起来能让人头疼死。

先说最基础的,除法和小数,SQL Server里,如果你直接写SELECT 1 / 2,猜猜结果是啥?不是0.5,而是0,因为SQL Server会看参与运算的数字是什么类型,如果两个都是整数,它就会做“整数除法”,直接把小数部分给扔了,想要得到0.5,你至少得把其中一个数变成小数,比如写成SELECT 1.0 / 2或者SELECT CAST(1 AS FLOAT) / 2,这还只是个开始。

当你用了小数类型,比如DECIMALNUMERIC(这俩在SQL Server里基本是一回事),新的麻烦又来了,你定义字段类型的时候,得指定精度(总共多少位数)和小数位数(小数点后多少位),比如DECIMAL(10, 2)表示总共10位数,小数点后占2位,问题在于,当你对两个不同精度和小数位数的DECIMAL值做计算时,SQL Server有一套复杂的规则来决定结果的精度和小数位,有时候算着算着,你可能发现结果的小数位数比你预想的要多,或者反而被截断了,如果不了解这些规则,盲目地把结果强制转成你想要的位数,就可能造成精度损失或者意想不到的四舍五入,做财务计算,每一分钱都得有出处,这种悄无声息的精度变化是绝对要避免的。

说到四舍五入,这里也是个坑,SQL Server默认的ROUND函数,有个参数可以指定是四舍五入还是直接截断,但你要知道,银行家舍入法(四舍六入五成双)和常见的四舍五入是不同的,虽然SQL Server用的不是银行家舍入,但你在设计系统时必须清楚业务上要求哪种舍入方式,更隐蔽的是,有时候你甚至没主动调用ROUND函数,但SQL Server在内部进行隐式类型转换或计算时,可能已经自动做了舍入,这可能会导致最终结果出现细微的偏差。

然后是聚合函数,比如算平均数AVG(),这里有个巨大的陷阱:AVG()函数的行为取决于你给它的是什么数据类型,如果你对一个整数类型的列(比如INT)求平均值,AVG()的结果默认会也是整数类型!它会先把所有值加起来,然后除以个数,但结果会被截断成整数,你想得到准确的小数平均值,就必须先把整数列转换成小数类型,比如AVG(CAST(YourColumn AS DECIMAL(10,2))),不知道这个细节的人,拿到的平均销量、平均评分之类的数据,从一开始就是错的。

还有精度更高的计算,比如计算百分比,常见的需求是,一组数各自占总和的百分比,并且要求这些百分比加起来正好是100%,如果你很自然地写SELECT Value / SUM(Value) OVER() * 100 AS Percentage,然后把这些Percentage加起来,很大概率会发现总和是99.98%或者100.01%,很难刚好是100%,这是因为每一步计算都有舍入误差,要达到完美,通常需要一些技巧,比如用“最大余量法”:先计算出基础的百分比(通常保留更多小数位),然后对小数部分进行排序,把因为舍入丢失的百分比点数,按顺序逐个分配给余量最大的那些项,从而把总和补足到100%,这种逻辑完全靠单纯的SQL语句写起来就非常复杂了。

再说说和钱相关的累计计算,比如算账户余额,你可能会用窗口函数SUM() OVER(ORDER BY Date)来累加每天的流水,这看起来很简单,但如果数据量巨大,这种排序累加的性能可能会成为瓶颈,更重要的是,你要确保交易记录的顺序是绝对正确的,如果ORDER BY的子句用的不对,导致某几笔交易的顺序错乱,那么计算出来的余额就会完全错误,在高并发的情况下,还要考虑事务的隔离级别,防止读到未提交的脏数据,不然A转账给B的过程中,别人来查总金额,可能看到的是转账前的状态,导致总金额对不上。

处理NULL值也是精准计算中必须小心的一点。NULL加任何数还是NULLNULL和任何数比较结果都是未知,如果你在计算中忽略了某个字段可能的NULL值,整个计算结果就可能变成NULL,所以通常要用ISNULL()COALESCE()函数给NULL一个默认值(比如0),但给什么值,又要根据具体的业务逻辑来定,不能想当然。

在SQL Server里做精准计算,绝不仅仅是写出正确的数学公式,它要求你对数据类型、内置函数的细微差别、SQL Server的隐式转换规则、以及聚合和窗口函数的执行机制有深入的了解,还必须结合业务逻辑,对精度、舍入方式和异常处理(如NULL值)做出明确且一致的规定,否则,看似简单的计算,背后可能埋藏着难以察觉的数据准确性问题。 结束)

用SQL Server做那些精准计算,运算细节其实没那么简单