Pourquoi 199,96 - 0 = 200 en SQL?

84

J'ai des clients qui reçoivent des factures bizarres. J'ai pu isoler le problème central:

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

Quelqu'un a-t-il une idée de ce qui se passe ici? Je veux dire, cela a certainement quelque chose à voir avec le type de données décimal, mais je ne peux pas vraiment comprendre cela ...


Il y avait beaucoup de confusion sur le type de données des littéraux numériques, j'ai donc décidé d'afficher la ligne réelle:

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)

Je me suis assuré que le résultat de chaque opération ayant un opérande d'un type différent de celui DECIMAL(19, 4)est casté explicitement avant de l'appliquer au contexte externe.

Néanmoins, le résultat demeure 200.00.


J'ai maintenant créé un échantillon résumé que vous pouvez exécuter sur votre ordinateur.

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

Maintenant, j'ai quelque chose ...

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

Que diable - floor est censé retourner un entier de toute façon. Que se passe t-il ici? :-RÉ


Je pense que j'ai maintenant réussi à vraiment le résumer à l'essence même :-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))
)
Silverdust
la source
4
@Sliverdust 199.96 -0 n'est pas égal à 200. Toutes ces transtypages, et tous ces étages avec des conversions implicites en virgule flottante et inversement sont garantis pour entraîner une perte de précision.
Panagiotis Kanavos
1
@Silverdust uniquement s'il provient d'une table. En tant que littéral dans une expression, c'est probablement unfloat
Panagiotis Kanavos
1
Oh ... et Floor()ne retourne pas un int. Elle renvoie le même type que l'expression d'origine , avec la partie décimale supprimée. Pour le reste, la IIF()fonction donne le type avec la priorité la plus élevée ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Ainsi, le deuxième échantillon dans lequel vous effectuez un cast en int, la priorité la plus élevée est la conversion simple en numérique (19,4).
Joel Coehoorn
1
Excellente réponse (qui savait que vous pouviez examiner les métadonnées d'une variante SQL?) Mais en 2012, j'obtiens les résultats attendus (199,96).
benjamin moskovits
2
Je ne suis pas trop familier avec MS SQL, mais je dois dire que regarder toutes ces opérations de conversion et ainsi de suite a rapidement attiré mon attention .. je dois donc lier cela car personne ne devrait jamais utiliser floatdes types de point ing pour gérer la monnaie .
code_dredd

Réponses:

78

Je dois commencer par déballer un peu ceci pour que je puisse voir ce qui se passe:

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

Voyons maintenant exactement quels types SQL Server utilise pour chaque côté de l'opération de soustraction:

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

Résultats:

numérique 5 2
numérique 38 1

Donc , 199.96est numeric(5,2)et plus Floor(Cast(etc))est numeric(38,1).

Les règles pour la précision et l'échelle résultantes d'une opération de soustraction (c'est-à-dire e1 - e2:) ressemblent à ceci:

Précision: max (s1, s2) + max (p1-s1, p2-s2) + 1
Echelle: max (s1, s2)

Cela évalue comme ceci:

Précision: max (1,2) + max (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
Échelle: max (1,2) => 2

Vous pouvez également utiliser le lien règles pour déterminer d'où numeric(38,1)vient le premier (indice: vous avez multiplié deux valeurs de précision 19).

Mais:

  • La précision et l'échelle du résultat ont un maximum absolu de 38. Lorsqu'une précision de résultat est supérieure à 38, elle est réduite à 38, et l'échelle correspondante est réduite pour essayer d'empêcher la partie intégrale d'un résultat d'être tronquée. Dans certains cas, tels que la multiplication ou la division, le facteur d'échelle ne sera pas réduit afin de conserver la précision décimale, bien que l'erreur de débordement puisse être augmentée.

Oups. La précision est de 40. Nous devons la réduire, et comme la réduction de la précision doit toujours couper les chiffres les moins significatifs, cela signifie également réduire l'échelle. Le type final résultant de l'expression sera numeric(38,0), qui pour les 199.96arrondis à 200.

Vous pouvez probablement résoudre ce problème en déplaçant et en consolidant les CAST()opérations de l'intérieur de la grande expression vers une opération CAST() autour du résultat de l'expression entière. Donc ça:

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

Devient:

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

Je pourrais même enlever le plâtre extérieur aussi.

Nous apprenons ici que nous devons choisir des types qui correspondent à la précision et à l'échelle que nous avons actuellement , plutôt qu'au résultat attendu. Cela n'a pas de sens de se limiter à des nombres de grande précision, car SQL Server mute ces types pendant les opérations arithmétiques pour éviter les débordements.


Plus d'information:

Stanislav Kundii
la source
20

Gardez un œil sur les types de données impliqués pour l'instruction suivante:

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)est NUMERIC(38, 7)(voir ci-dessous)
    • FLOOR(NUMERIC(38, 7))est NUMERIC(38, 0)(voir ci-dessous)
  2. 0.0 est NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0) est NUMERIC(38, 1)
  3. 199.96 est NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)est NUMERIC(38, 1)(voir ci-dessous)

Cela explique pourquoi vous vous retrouvez avec 200.0( un chiffre après la décimale, pas zéro ) au lieu de 199.96.

Remarques:

FLOORrenvoie le plus grand entier inférieur ou égal à l'expression numérique spécifiée et le résultat a le même type que l'entrée. Il renvoie INT pour INT, FLOAT pour FLOAT et NUMERIC (x, 0) pour NUMERIC (x, y).

Selon l'algorithme :

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)

* La précision et l'échelle du résultat ont un maximum absolu de 38. Lorsqu'une précision de résultat est supérieure à 38, elle est réduite à 38, et l'échelle correspondante est réduite pour essayer d'empêcher la partie intégrale d'un résultat d'être tronquée.

La description contient également des détails sur la manière exacte dont l'échelle est réduite dans les opérations d'addition et de multiplication. Sur la base de cette description:

  • NUMERIC(19, 4) * NUMERIC(19, 4)est NUMERIC(39, 8)et serré àNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)est NUMERIC(40, 1)et serré àNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)est NUMERIC(40, 2)et serré àNUMERIC(38, 1)

Voici ma tentative d'implémentation de l'algorithme en JavaScript. J'ai comparé les résultats à SQL Server. Cela répond à l' essentiel de votre 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);

Salman A
la source