为什么10 ^ 37/1会引发算术溢出错误?


11

继续我最近玩大量数字的趋势,我最近将错误归结为以下代码:

DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);

PRINT @big_number + 1;
PRINT @big_number - 1;
PRINT @big_number * 1;
PRINT @big_number / 1;

我得到的这段代码的输出是:

10000000000000000000000000000000000001
9999999999999999999999999999999999999
10000000000000000000000000000000000000
Msg 8115, Level 16, State 2, Line 6
Arithmetic overflow error converting expression to data type numeric.

什么?

为什么前三个操作有效,但最后一个无效?如果@big_number可以明显地存储的输出,怎么会有算术溢出错误@big_number / 1

Answers:


18

在算术运算中了解精度和小数位数

让我们分解一下,仔细看一下除法算术运算符的细节。这是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更高,因此我们知道该部门的输出将投射到。 INTDECIMAL

那么问题出在哪里呢?

问题在于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
;

那里有。小心分开,我的孩子们。



2
RE:“更清洁的方式”。您可能想看看SQL_VARIANT_PROPERTY
Martin Smith

@Martin-您能否提供一个示例或快速说明,SQL_VARIANT_PROPERTY以说明如何使用像问题中讨论的那样进行除法?
Nick Chammas 2012年

1
这里有一个示例(作为创建新表以确定数据类型的替代方法)
Martin Smith

@Martin-啊,是的,整洁了!
Nick Chammas 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.