Pourquoi 10 ^ 37/1 génère-t-il une erreur de dépassement arithmétique?


11

Poursuivant ma récente tendance à jouer avec de grands nombres , j'ai récemment fait bouillir une erreur que je rencontrais jusqu'au code suivant:

DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);

PRINT @big_number + 1;
PRINT @big_number - 1;
PRINT @big_number * 1;
PRINT @big_number / 1;

La sortie que j'obtiens pour ce code est:

10000000000000000000000000000000000001
9999999999999999999999999999999999999
10000000000000000000000000000000000000
Msg 8115, Level 16, State 2, Line 6
Arithmetic overflow error converting expression to data type numeric.

Quelle?

Pourquoi les 3 premières opérations fonctionneraient-elles mais pas la dernière? Et comment peut-il y avoir une erreur de dépassement arithmétique si @big_numberpeut évidemment stocker la sortie de @big_number / 1?

Réponses:


18

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_numberc'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 1dans 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 DECIMALet 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 . INTDECIMAL

Alors où est le problème?

Le problème est avec l'échelle de DECIMALla 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 DECIMALpeut 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 DECIMALs 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 FLOATpour les calculs, puis de les reconvertir DECIMALlorsque 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 FLOATest dangereuse et peut vous donner des résultats inattendus ou incorrects.

Dans notre cas, la conversion de 10 37 vers et depuis FLOATobtient 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.



2
RE: "Cleaner Way". Vous voudrez peut-être regarderSQL_VARIANT_PROPERTY
Martin Smith

@Martin - Pourriez-vous fournir un exemple ou une explication rapide de la façon dont je pourrais utiliser SQL_VARIANT_PROPERTYpour effectuer des divisions comme celle discutée dans la question?
Nick Chammas

1
Il y a un exemple ici (comme alternative à la création d'une nouvelle table pour déterminer le type de données)
Martin Smith

@Martin - Ah oui, c'est beaucoup plus soigné!
Nick Chammas
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.