【问题标题】:Why is 199.96 - 0 = 200 in SQL?为什么 199.96 - 0 = 200 在 SQL 中?
【发布时间】:2018-12-28 18:31:07
【问题描述】:

我有一些客户收到奇怪的账单。我能够隔离核心问题:

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)))

到底是什么 - floor 无论如何都应该返回一个整数。这里发生了什么? :-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))
)

【问题讨论】:

  • @Sliverdust 199.96 -0 不等于 200。所有这些转换,以及隐式转换为浮点数和返回的地板都保证会导致精度损失。
  • @Silverdust 仅当它来自桌子时。作为表达式中的文字,它可能是 float
  • 哦...而Floor() 确实返回int。它返回 与原始表达式相同的类型,但已删除小数部分。对于其余部分,IIF() 函数会生成具有最高优先级的类型 (docs.microsoft.com/en-us/sql/t-sql/functions/…)。因此,您转换为 int 的第二个样本,较高的优先级是简单转换为 numeric(19,4)。
  • 很好的答案(谁知道您可以检查 sql 变体的元数据?)但在 2012 年我得到了预期的结果 (199.96)。
  • 我对 MS SQL 不太熟悉,但我必须说,看到所有这些强制转换操作等很快就引起了我的注意.. 所以我必须link this 因为没有人应该 曾经使用floating-point 类型来处理货币。

标签: sql-server tsql precision sql-server-2016 sqldatatypes


【解决方案1】:

留意以下语句所涉及的数据类型:

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)。

根据the algorithm

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);

【讨论】:

    【解决方案2】:

    我需要先把它打开一点,这样我才能看到发生了什么:

    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)

    减法运算的rules for the resulting precision and scale(即: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 会在算术运算期间改变这些类型以避免溢出。


    更多信息:

    【讨论】:

      猜你喜欢
      • 2017-06-03
      • 1970-01-01
      • 1970-01-01
      • 2012-02-26
      • 1970-01-01
      • 2018-05-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-19
      相关资源
      最近更新 更多