Warum löst 10 ^ 37/1 einen arithmetischen Überlauffehler aus?

11

Als Fortsetzung meines jüngsten Trends , mit großen Zahlen zu spielen , habe ich kürzlich einen Fehler, auf den ich gestoßen bin, auf den folgenden Code reduziert:

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;

Die Ausgabe, die ich für diesen Code bekomme, ist:

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

Was?

Warum sollten die ersten drei Operationen funktionieren, aber nicht die letzten? Und wie kann es zu einem arithmetischen Überlauffehler kommen, wenn @big_numberdie Ausgabe von offensichtlich gespeichert werden kann @big_number / 1?

Nick Chammas
quelle

Antworten:

18

Verständnis von Präzision und Skalierung im Kontext von arithmetischen Operationen

Lassen Sie uns diese nach unten brechen und einen genauen Blick auf die Details der Kluft nehmen Rechenzeichen . Dies ist, was MSDN zu den Ergebnistypen des Divide-Operators zu sagen hat :

Ergebnistypen

Gibt den Datentyp des Arguments mit der höheren Priorität zurück. Weitere Informationen finden Sie unter Datentypvorrang (Transact-SQL). .

Wenn eine Ganzzahldividende durch einen Ganzzahldivisor geteilt wird, ist das Ergebnis eine Ganzzahl, bei der ein Bruchteil des Ergebnisses abgeschnitten ist.

Wir wissen, dass @big_numberdas ein ist DECIMAL. Als welchen Datentyp wird SQL Server umgewandelt 1? Es wirft es zu einem INT. Wir können dies mit Hilfe von bestätigen 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
;

Für Kicks können wir auch den 1im ursprünglichen Codeblock enthaltenen durch einen explizit eingegebenen Wert wie ersetzenDECLARE @one INT = 1; und bestätigen, dass wir die gleichen Ergebnisse erhalten.

Also haben wir ein DECIMALund ein INT. Da DECIMALder Datentyp eine höhere Priorität hat als INT, wissen wir, dass die Ausgabe unserer Abteilung in umgewandelt wirdDECIMAL .

Wo ist das Problem?

Das Problem liegt in der Skalierung der DECIMALin der Ausgabe. Hier ist eine Regeltabelle darüber, wie SQL Server die Genauigkeit und Skalierung von Ergebnissen bestimmt, die aus arithmetischen Operationen erhalten werden:

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.

Und hier ist, was wir für die Variablen in dieser Tabelle haben:

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

Gemäß dem Sternchenkommentar in der obigen Tabelle beträgt die maximale Genauigkeit, die a haben DECIMALkann, 38 . Daher wird unsere Ergebnisgenauigkeit von 49 auf 38 verringert und "die entsprechende Skala wird reduziert, um zu verhindern, dass der integrale Bestandteil eines Ergebnisses abgeschnitten wird." Aus diesem Kommentar geht nicht hervor, wie sich die Skala verringert, aber wir wissen Folgendes:

Gemäß der Formel in der Tabelle beträgt die minimal mögliche Skala, die Sie nach dem Teilen von zwei DECIMALs haben können, 6.

Somit erhalten wir die folgenden Ergebnisse:

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.

Wie dies den arithmetischen Überlauf erklärt

Jetzt liegt die Antwort auf der Hand:

Die Ausgabe unserer Abteilung wird auf 10 37 gesetztDECIMAL(38, 6) und DECIMAL(38, 6)kann diese nicht halten .

Damit können wir eine weitere Teilung konstruieren, indem sichergestellt wird das Ergebnis gelingt es kann passen 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;

Das Ergebnis ist:

10000000000000000000000000000000.000000

Beachten Sie die 6 Nullen nach der Dezimalstelle. Wir können das Datentyp des Ergebnisses bestätigen ist DECIMAL(38, 6)durch die Verwendung SQL_VARIANT_PROPERTY()wie oben:

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
;

Eine gefährliche Problemumgehung

Wie umgehen wir diese Einschränkung?

Nun, das hängt sicherlich davon ab, wofür Sie diese Berechnungen durchführen. Eine Lösung, zu der Sie sofort springen können, besteht darin, Ihre Zahlen FLOATfür die Berechnungen in zu konvertieren und sie dann wieder zu konvertieren, DECIMALwenn Sie fertig sind.

Das mag unter bestimmten Umständen funktionieren, aber Sie sollten vorsichtig sein, um diese Umstände zu verstehen. Wie wir alle wissen, ist das Konvertieren von Zahlen von und nach FLOATgefährlich und kann zu unerwarteten oder falschen Ergebnissen führen.

In unserem Fall führt die Konvertierung von 10 37 nach und von zu FLOATeinem Ergebnis, das einfach falsch ist :

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
;

Und da hast du es. Teile dich sorgfältig, meine Kinder.

Nick Chammas
quelle
2
RE: "Cleaner Way". Vielleicht möchten Sie sich ansehenSQL_VARIANT_PROPERTY
Martin Smith
@Martin - Können Sie ein Beispiel oder eine kurze Erklärung geben, wie ich SQL_VARIANT_PROPERTYDivisionen wie die in der Frage beschriebene durchführen kann?
Nick Chammas
1
Hier gibt es ein Beispiel (als Alternative zum Erstellen einer neuen Tabelle zur Bestimmung des Datentyps)
Martin Smith
@ Martin - Ah ja, das ist viel ordentlicher!
Nick Chammas