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))
)
la source
float
Floor()
ne retourne pas unint
. Elle renvoie le même type que l'expression d'origine , avec la partie décimale supprimée. Pour le reste, laIIF()
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).float
des types de point ing pour gérer la monnaie .Réponses:
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:
Donc ,
199.96
estnumeric(5,2)
et plusFloor(Cast(etc))
estnumeric(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:Cela évalue comme ceci:
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:
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 les199.96
arrondis à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érationCAST()
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:
Sql_Variant_Property()
la source
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))))
NUMERIC(19, 4) * NUMERIC(19, 4)
estNUMERIC(38, 7)
(voir ci-dessous)FLOOR(NUMERIC(38, 7))
estNUMERIC(38, 0)
(voir ci-dessous)0.0
estNUMERIC(1, 1)
NUMERIC(1, 1) * NUMERIC(38, 0)
estNUMERIC(38, 1)
199.96
estNUMERIC(5, 2)
NUMERIC(5, 2) - NUMERIC(38, 1)
estNUMERIC(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 de199.96
.Remarques:
FLOOR
renvoie 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 :
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)
estNUMERIC(39, 8)
et serré àNUMERIC(38, 7)
NUMERIC(1, 1) * NUMERIC(38, 0)
estNUMERIC(40, 1)
et serré àNUMERIC(38, 1)
NUMERIC(5, 2) - NUMERIC(38, 1)
estNUMERIC(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);
la source