在算术运算中了解精度和小数位数
让我们分解一下,仔细看一下除法算术运算符的细节。这是MSDN关于除法运算符的结果类型的说法:
结果类型
返回优先级较高的参数的数据类型。有关更多信息,请参见数据类型优先级(Transact-SQL)。
如果将整数除数除以整数除数,则结果将是结果的任何小数部分均被截断的整数。
我们知道那@big_number
是一个DECIMAL
。SQL Server强制转换1
为哪种数据类型?它将其转换为INT
。我们可以借助SQL_VARIANT_PROPERTY()
:
SELECT
SQL_VARIANT_PROPERTY(1, 'BaseType') AS [BaseType] -- int
, SQL_VARIANT_PROPERTY(1, 'Precision') AS [Precision] -- 10
, SQL_VARIANT_PROPERTY(1, 'Scale') AS [Scale] -- 0
;
对于kicks,我们还可以1
将原始代码块中的替换为显式键入的值,例如,DECLARE @one INT = 1;
并确认获得相同的结果。
因此,我们有一个DECIMAL
和一个INT
。由于数据类型优先级比DECIMAL
更高,因此我们知道该部门的输出将投射到。 INT
DECIMAL
那么问题出在哪里呢?
问题在于DECIMAL
输出中的比例。下表是有关SQL Server如何确定从算术运算获得的结果的精度和小数位数的规则表:
Operation Result precision Result scale *
-------------------------------------------------------------------------------------------------
e1 + e2 max(s1, s2) + max(p1-s1, p2-s2) + 1 max(s1, s2)
e1 - e2 max(s1, s2) + max(p1-s1, p2-s2) + 1 max(s1, s2)
e1 * e2 p1 + p2 + 1 s1 + s2
e1 / e2 p1 - s1 + s2 + max(6, s1 + p2 + 1) max(6, s1 + p2 + 1)
e1 { UNION | EXCEPT | INTERSECT } e2 max(s1, s2) + max(p1-s1, p2-s2) max(s1, s2)
e1 % e2 min(p1-s1, p2 -s2) + max( s1,s2 ) max(s1, s2)
* The result precision and scale have an absolute maximum of 38. When a result
precision is greater than 38, the corresponding scale is reduced to prevent the
integral part of a result from being truncated.
这是此表中变量的内容:
e1: @big_number, a DECIMAL(38, 0)
-> p1: 38
-> s1: 0
e2: 1, an INT
-> p2: 10
-> s2: 0
e1 / e2
-> Result precision: p1 - s1 + s2 + max(6, s1 + p2 + 1) = 38 + max(6, 11) = 49
-> Result scale: max(6, s1 + p2 + 1) = max(6, 11) = 11
根据上表中的星号注释,a DECIMAL
可以具有的最大精度为38。因此,我们的结果精度从49降低到38,并且“相应的比例尺减小了,以防止结果的整数部分被截断”。从此评论尚不清楚如何减小比例,但是我们知道这一点:
根据表格中的公式,除以2 s 后可以拥有的最小标度DECIMAL
为6。
因此,我们得出以下结果:
e1 / e2
-> Result precision: 49 -> reduced to 38
-> Result scale: 11 -> reduced to 6
Note that 6 is the minimum possible scale it can be reduced to.
It may be between 6 and 11 inclusive.
这如何解释算术溢出
现在答案很明显:
我们部门的输出会投地DECIMAL(38, 6)
,并DECIMAL(38, 6)
不能容纳10 37。
就这样,我们就可以构造另一个部门,通过确保结果成功可以适应DECIMAL(38, 6)
:
DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million INT = '1' + REPLICATE(0, 6);
PRINT @big_number / @one_million;
结果是:
10000000000000000000000000000000.000000
注意小数点后的6个零。我们可以确认结果的数据类型是DECIMAL(38, 6)
通过使用SQL_VARIANT_PROPERTY()
如上:
DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million INT = '1' + REPLICATE(0, 6);
SELECT
SQL_VARIANT_PROPERTY(@big_number / @one_million, 'BaseType') AS [BaseType] -- decimal
, SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Precision') AS [Precision] -- 38
, SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Scale') AS [Scale] -- 6
;
危险的解决方法
那么我们如何解决这个限制呢?
好吧,这当然取决于您要进行这些计算的目的。您可能会立即跳到的一种解决方案是将数字转换为FLOAT
用于计算,然后DECIMAL
在完成后将其转换回。
在某些情况下这可能会起作用,但是您应该小心了解这些情况。众所周知,在数字之间来回转换FLOAT
是很危险的,可能会给您带来意想不到的错误结果。
在我们的例子中,将10 37与往返FLOAT
进行转换得到的结果是完全错误的:
DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @big_number_f FLOAT = CAST(@big_number AS FLOAT);
SELECT
@big_number AS big_number -- 10^37
, @big_number_f AS big_number_f -- 10^37
, CAST(@big_number_f AS DECIMAL(38, 0)) AS big_number_f_d -- 9999999999999999.5 * 10^21
;
那里有。小心分开,我的孩子们。