Why is 199.96 - 0 = 200 in SQL?

前端 未结 2 881
我寻月下人不归
我寻月下人不归 2021-01-31 13:14

I have some clients getting weird bills. I was able to isolate the core problem:

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


        
2条回答
  •  春和景丽
    2021-01-31 13:54

    Keep an eye on data types involved for the following statement:

    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) is NUMERIC(38, 7) (see below)
      • FLOOR(NUMERIC(38, 7)) is NUMERIC(38, 0) (see below)
    2. 0.0 is NUMERIC(1, 1)
      • NUMERIC(1, 1) * NUMERIC(38, 0) is NUMERIC(38, 1)
    3. 199.96 is NUMERIC(5, 2)
      • NUMERIC(5, 2) - NUMERIC(38, 1) is NUMERIC(38, 1) (see below)

    This explains why you end up with 200.0 (one digit after decimal, not zero) instead of 199.96.

    Notes:

    FLOOR returns the largest integer less than or equal to the specified numeric expression and result has the same type as input. It returns INT for INT, FLOAT for FLOAT and NUMERIC(x, 0) for NUMERIC(x, y).

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

    * The result precision and scale have an absolute maximum of 38. When a result precision is greater than 38, it is reduced to 38, and the corresponding scale is reduced to try to prevent the integral part of a result from being truncated.

    The description also contains the details of how exactly the scale is reduced inside addition and multiplication operations. Based on that description:

    • NUMERIC(19, 4) * NUMERIC(19, 4) is NUMERIC(39, 8) and clamped to NUMERIC(38, 7)
    • NUMERIC(1, 1) * NUMERIC(38, 0) is NUMERIC(40, 1) and clamped to NUMERIC(38, 1)
    • NUMERIC(5, 2) - NUMERIC(38, 1) is NUMERIC(40, 2) and clamped to NUMERIC(38, 1)

    Here is my attempt to implement the algorithm in JavaScript. I have cross checked the results against SQL Server. It answers the very essence part of your question.

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

提交回复
热议问题