Comprendre la précision et l'échelle dans le contexte des opérations arithmétiques
Décomposons cela et examinons de près les détails de l' opérateur arithmétique de division . Voici ce que MSDN a à dire sur les types de résultats de l' opérateur de division :
Types de résultats
Renvoie le type de données de l'argument avec la priorité la plus élevée. Pour plus d'informations, consultez Priorité des types de données (Transact-SQL) .
Si un dividende entier est divisé par un diviseur entier, le résultat est un entier dont toute partie fractionnaire du résultat est tronquée.
Nous savons que @big_number
c'est un DECIMAL
. Quel type de données est converti en SQL Server 1
? Il le transforme en un INT
. Nous pouvons le confirmer avec l'aide de SQL_VARIANT_PROPERTY()
:
SELECT
SQL_VARIANT_PROPERTY(1, 'BaseType') AS [BaseType] -- int
, SQL_VARIANT_PROPERTY(1, 'Precision') AS [Precision] -- 10
, SQL_VARIANT_PROPERTY(1, 'Scale') AS [Scale] -- 0
;
Pour les coups de pied, nous pouvons également remplacer le 1
dans le bloc de code d'origine par une valeur explicitement typée comme DECLARE @one INT = 1;
et confirmer que nous obtenons les mêmes résultats.
Nous avons donc un DECIMAL
et un INT
. Étant donné que la priorité du type de donnéesDECIMAL
est supérieure à , nous savons que la sortie de notre division sera convertie en . INT
DECIMAL
Alors où est le problème?
Le problème est avec l'échelle de DECIMAL
la sortie. Voici un tableau de règles sur la façon dont SQL Server détermine la précision et l'échelle des résultats obtenus à partir d'opérations arithmétiques:
Operation Result precision Result scale *
-------------------------------------------------------------------------------------------------
e1 + e2 max(s1, s2) + max(p1-s1, p2-s2) + 1 max(s1, s2)
e1 - e2 max(s1, s2) + max(p1-s1, p2-s2) + 1 max(s1, s2)
e1 * e2 p1 + p2 + 1 s1 + s2
e1 / e2 p1 - s1 + s2 + max(6, s1 + p2 + 1) max(6, s1 + p2 + 1)
e1 { UNION | EXCEPT | INTERSECT } e2 max(s1, s2) + max(p1-s1, p2-s2) max(s1, s2)
e1 % e2 min(p1-s1, p2 -s2) + max( s1,s2 ) max(s1, s2)
* The result precision and scale have an absolute maximum of 38. When a result
precision is greater than 38, the corresponding scale is reduced to prevent the
integral part of a result from being truncated.
Et voici ce que nous avons pour les variables de ce tableau:
e1: @big_number, a DECIMAL(38, 0)
-> p1: 38
-> s1: 0
e2: 1, an INT
-> p2: 10
-> s2: 0
e1 / e2
-> Result precision: p1 - s1 + s2 + max(6, s1 + p2 + 1) = 38 + max(6, 11) = 49
-> Result scale: max(6, s1 + p2 + 1) = max(6, 11) = 11
Selon le commentaire de l'astérisque dans le tableau ci-dessus, la précision maximale que DECIMAL
peut avoir est 38 . Ainsi, la précision de nos résultats passe de 49 à 38, et «l'échelle correspondante est réduite pour éviter que la partie intégrante d'un résultat ne soit tronquée». Il n'est pas clair de ce commentaire comment l'échelle est réduite, mais nous le savons:
Selon la formule du tableau, l' échelle minimale possible que vous pouvez avoir après avoir divisé deux DECIMAL
s est 6.
Ainsi, nous nous retrouvons avec les résultats suivants:
e1 / e2
-> Result precision: 49 -> reduced to 38
-> Result scale: 11 -> reduced to 6
Note that 6 is the minimum possible scale it can be reduced to.
It may be between 6 and 11 inclusive.
Comment cela explique le dépassement arithmétique
Maintenant, la réponse est évidente:
La sortie de notre division est castée DECIMAL(38, 6)
et DECIMAL(38, 6)
ne peut pas contenir 10 37 .
Avec cela, nous pouvons construire une autre division qui réussit en s'assurant que le résultat peut s'intégrer DECIMAL(38, 6)
:
DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million INT = '1' + REPLICATE(0, 6);
PRINT @big_number / @one_million;
Le résultat est:
10000000000000000000000000000000.000000
Notez les 6 zéros après la décimale. Nous pouvons confirmer que le type de données du résultat est DECIMAL(38, 6)
en utilisant SQL_VARIANT_PROPERTY()
comme ci-dessus:
DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million INT = '1' + REPLICATE(0, 6);
SELECT
SQL_VARIANT_PROPERTY(@big_number / @one_million, 'BaseType') AS [BaseType] -- decimal
, SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Precision') AS [Precision] -- 38
, SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Scale') AS [Scale] -- 6
;
Une solution de contournement dangereuse
Alors, comment contourner cette limitation?
Eh bien, cela dépend certainement de la raison pour laquelle vous faites ces calculs. Une solution à laquelle vous pouvez immédiatement passer est de convertir vos nombres en FLOAT
pour les calculs, puis de les reconvertir DECIMAL
lorsque vous avez terminé.
Cela peut fonctionner dans certaines circonstances, mais vous devez faire attention à comprendre quelles sont ces circonstances. Comme nous le savons tous, la conversion de nombres vers et depuis FLOAT
est dangereuse et peut vous donner des résultats inattendus ou incorrects.
Dans notre cas, la conversion de 10 37 vers et depuis FLOAT
obtient un résultat qui est tout simplement faux :
DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @big_number_f FLOAT = CAST(@big_number AS FLOAT);
SELECT
@big_number AS big_number -- 10^37
, @big_number_f AS big_number_f -- 10^37
, CAST(@big_number_f AS DECIMAL(38, 0)) AS big_number_f_d -- 9999999999999999.5 * 10^21
;
Et voila. Répartissez soigneusement, mes enfants.
SQL_VARIANT_PROPERTY
SQL_VARIANT_PROPERTY
pour effectuer des divisions comme celle discutée dans la question?