为什么在SQL中为199.96-0 = 200?


84

我有些客户的账单很奇怪。我能够找出核心问题:

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 200 what the?
SELECT 199.96 - (0.0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96)) -- 199.96

SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 199.96

-- It gets weirder...
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 0
SELECT (0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 0
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 0

-- so... ... 199.06 - 0 equals 200... ... right???
SELECT 199.96 - 0 -- 199.96 ...NO....

有任何线索,这到底是怎么回事?我的意思是,这肯定与十进制数据类型有关,但是我实在无法绕开它……


关于数字字面量是什么数据类型存在很多困惑,所以我决定显示真实的行:

PS.SharePrice - (CAST((@InstallmentCount - 1) AS DECIMAL(19, 4)) * CAST(FLOOR(@InstallmentPercent * PS.SharePrice) AS DECIMAL(19, 4))))

PS.SharePrice DECIMAL(19, 4)

@InstallmentCount INT

@InstallmentPercent DECIMAL(19, 4)

DECIMAL(19, 4)在将其应用于外部上下文之前,我确保每个操作的结果都具有与显式转换的类型不同的操作数。

尽管如此,结果仍然存在200.00


现在,我创建了一个简化的示例,您可以在计算机上执行。

DECLARE @InstallmentIndex INT = 1
DECLARE @InstallmentCount INT = 1
DECLARE @InstallmentPercent DECIMAL(19, 4) = 1.0
DECLARE @PS TABLE (SharePrice DECIMAL(19, 4))
INSERT INTO @PS (SharePrice) VALUES (599.96)

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * PS.SharePrice),
  1999.96)
FROM @PS PS

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(599.96 AS DECIMAL(19, 4))),
  1999.96)
FROM @PS PS

-- 1996.96
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * 599.96),
  1999.96)
FROM @PS PS

-- Funny enough - with this sample explicitly converting EVERYTHING to DECIMAL(19, 4) - it still doesn't work...
-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))
FROM @PS PS

现在我有东西了

-- 2000
SELECT
  IIF(1 = 2,
  FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))

-- 1999.9600
SELECT
  IIF(1 = 2,
  CAST(FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) AS INT),
  CAST(1999.96 AS DECIMAL(19, 4)))

到底该怎么办?地板应该返回一个整数。这里发生了什么?:-D


我想我现在已经把它真正地归结为本质了:-D

-- 1.96
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (36, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2.0
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (37, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (38, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

4
@Sliverdust 199.96 -0不等于200。尽管如此,所有这些强制转换和地板以及隐式转换为浮点数和返回值的转换都可以确保导致精度损失。
帕纳吉奥提斯·卡纳沃斯

1
@Silverdust仅来自表中。作为一种表达字面它可能是一个float
帕纳约蒂斯Kanavos

1
呵呵......并且Floor()没有返回int。它返回与原始表达式相同的类型,但删除了小数部分。对于其余部分,该IIF()函数将导致优先级最高的类型(docs.microsoft.com/en-us/sql/t-sql/functions/…)。因此,将第二个样本强制转换为int时,优先级较高的是将简单强制转换为numeric(19,4)。
Joel Coehoorn '18

1
很好的答案(谁知道您可以检查sql变体的元数据?),但在2012年,我得到了预期的结果(199.96)。
benjamin moskovits

2
我不是太熟悉MS SQL,但我必须说,看着那些投业务等迅速引起了我的注意..所以我必须链接这是因为任何人都不应该永远使用float荷兰国际集团点的类型来处理货币。
code_dredd

Answers:


78

我需要先对此展开一些内容,以便可以看到发生了什么:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

现在,让我们确切地了解一下SQL Server在减法操作的每一侧使用什么类型:

SELECT  SQL_VARIANT_PROPERTY (199.96     ,'BaseType'),
    SQL_VARIANT_PROPERTY (199.96     ,'Precision'),
    SQL_VARIANT_PROPERTY (199.96     ,'Scale')

SELECT  SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'BaseType'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Precision'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Scale')

结果:

数值5 2
数值38 1

所以,199.96numeric(5,2)的时间越长Floor(Cast(etc))numeric(38,1)

减法运算的精度和小数位数的规则(即e1 - e2:)如下所示:

精度: max(s1,s2)+ max(p1-s1,p2-s2)+1
比例尺: max(s1,s2)

评估结果如下:

精度: max(1,2)+ max(38-1,5-2)+1 => 2 + 37 +1 => 40
比例尺: max(1,2)=> 2

您还可以使用“规则”链接首先找出numeric(38,1)来源(提示:您将两个精度19值相乘)。

但:

  • 结果精度和小数位数的绝对最大值为38。当结果精度大于38时,它将减小为38,并减小相应的小数位数,以防止结果的整数部分被截断。在某些情况下,例如乘法或除法,尽管会增加溢出误差,但为了保持十进制精度,将不会减小比例因子。

哎呀。精度为40。我们必须降低精度,因为降低精度应始终切断最不重要的数字,这也意味着缩小比例。表达式的最终结果类型为numeric(38,0)199.96舍入为200

您可以通过移动和巩固可能解决这个问题CAST(),从大的表达式中操作一个 CAST()围绕整个表达式的结果。所以这:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

成为:

SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))

我什至可以删除外部演员表。

我们在这里学习,我们应该选择的类型相匹配的精度和规模,我们其实有现在,而不是预期的结果。仅使用较大的精度数字是没有意义的,因为SQL Server会在算术运算期间对那些类型进行突变以尝试避免溢出。


更多信息:


20

请密切注意涉及以下语句的数据类型:

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))))
  1. NUMERIC(19, 4) * NUMERIC(19, 4)NUMERIC(38, 7)(请参见下文)
    • FLOOR(NUMERIC(38, 7))NUMERIC(38, 0)(请参见下文)
  2. 0.0NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0)NUMERIC(38, 1)
  3. 199.96NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)NUMERIC(38, 1)(请参见下文)

这解释了为什么您最终以200.0小数点后一位不是零)而不是199.96

笔记:

FLOOR返回小于或等于指定数字表达式的最大整数,并且结果与输入具有相同的类型。它为INT返回INT,为FLOAT返回FLOAT,为NUMERIC(x,y)返回NUMERIC(x,0)。

根据算法

Operation | Result precision                    | Result scale*
e1 * e2   | p1 + p2 + 1                         | s1 + s2
e1 - e2   | max(s1, s2) + max(p1-s1, p2-s2) + 1 | max(s1, s2)

*结果精度和小数位数的绝对最大值为38。当结果精度大于38时,它将减小为38,并减小相应的小数位数,以防止结果的整数部分被截断。

该说明还包含如何在加法和乘法运算中精确缩小比例的细节。根据该说明:

  • NUMERIC(19, 4) * NUMERIC(19, 4)NUMERIC(39, 8)并且钳位到NUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)NUMERIC(40, 1)并且钳位到NUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)NUMERIC(40, 2)并且钳位到NUMERIC(38, 1)

这是我尝试在JavaScript中实现算法的尝试。我已经对照SQL Server对结果进行了交叉检查。它回答了您问题的实质部分。

// https://docs.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql?view=sql-server-2017

function numericTest_mul(p1, s1, p2, s2) {
  // e1 * e2
  var precision = p1 + p2 + 1;
  var scale = s1 + s2;

  // see notes in the linked article about multiplication operations
  var newscale;
  if (precision - scale < 32) {
    newscale = Math.min(scale, 38 - (precision - scale));
  } else if (scale < 6 && precision - scale > 32) {
    newscale = scale;
  } else if (scale > 6 && precision - scale > 32) {
    newscale = 6;
  }

  console.log("NUMERIC(%d, %d) * NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_add(p1, s1, p2, s2) {
  // e1 + e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2) + 1;
  var scale = Math.max(s1, s2);

  // see notes in the linked article about addition operations
  var newscale;
  if (Math.max(p1 - s1, p2 - s2) > Math.min(38, precision) - scale) {
    newscale = Math.min(precision, 38) - Math.max(p1 - s1, p2 - s2);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_union(p1, s1, p2, s2) {
  // e1 UNION e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2);
  var scale = Math.max(s1, s2);

  // my idea of how newscale should be calculated, not official
  var newscale;
  if (precision > 38) {
    newscale = scale - (precision - 38);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

/*
 * first example in question
 */

// CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))
numericTest_mul(19, 4, 19, 4);

// 0.0 * FLOOR(...)
numericTest_mul(1, 1, 38, 0);

// 199.96 * ...
numericTest_add(5, 2, 38, 1);

/*
 * IIF examples in question
 * the logic used to determine result data type of IIF / CASE statement
 * is same as the logic used inside UNION operations
 */

// FLOOR(DECIMAL(38, 7)) UNION CAST(1999.96 AS DECIMAL(19, 4)))
numericTest_union(38, 0, 19, 4);

// CAST(1.0 AS DECIMAL (36, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(36, 0, 19, 4);

// CAST(1.0 AS DECIMAL (37, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(37, 0, 19, 4);

// CAST(1.0 AS DECIMAL (38, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(38, 0, 19, 4);

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.