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

Nic*_*mas 11 sql-server-2008 sql-server datatypes

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

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;
Run Code Online (Sandbox Code Playgroud)

我为此代码得到的输出是:

10000000000000000000000000000000000001
9999999999999999999999999999999999999
10000000000000000000000000000000000000
Msg 8115, Level 16, State 2, Line 6
Arithmetic overflow error converting expression to data type numeric.
Run Code Online (Sandbox Code Playgroud)

什么?

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

Nic*_*mas 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
;
Run Code Online (Sandbox Code Playgroud)

对于踢球,我们还可以1用显式键入的值替换原始代码块中的 ,DECLARE @one INT = 1;并确认我们得到相同的结果。

所以我们有 aDECIMAL和 an 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.
Run Code Online (Sandbox Code Playgroud)

这是我们对这个表中的变量所拥有的:

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
Run Code Online (Sandbox Code Playgroud)

根据上表中的星号注释,aDECIMAL可以具有的最大精度为 38。所以我们的结果精度从 49 减少到 38,并且“相应的比例被减少以防止结果的整数部分被截断”。从这个评论中不清楚规模是如何缩小的,但我们确实知道这一点:

根据表中的公式,将两个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.
Run Code Online (Sandbox Code Playgroud)

这如何解释算术溢出

现在答案显而易见:

我们部门的输出被强制转换为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;
Run Code Online (Sandbox Code Playgroud)

结果是:

10000000000000000000000000000000.000000
Run Code Online (Sandbox Code Playgroud)

注意小数点后的 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
;
Run Code Online (Sandbox Code Playgroud)

危险的解决方法

那么我们如何绕过这个限制呢?

嗯,这当然取决于您进行这些计算的目的。您可能会立即跳转到的一种解决方案是将您的数字转换为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
;
Run Code Online (Sandbox Code Playgroud)

你有它。小心分开,我的孩子们。

  • [这里有一些关于截断的更多细节](http://blogs.msdn.com/b/sqlprogrammability/archive/2006/03/29/564110.aspx) (2认同)
  • RE:“更清洁的方式”。你可能想看看 [`SQL_VARIANT_PROPERTY`](http://stackoverflow.com/a/7428636/73226) (2认同)