Perché 10 ^ 37/1 genera un errore di overflow aritmetico?


11

Continuando la mia recente tendenza a giocare con numeri grandi , di recente ho bollato un errore che stavo riscontrando fino al seguente codice:

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;

L'output che ottengo per questo codice è:

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

Che cosa?

Perché le prime 3 operazioni dovrebbero funzionare ma non le ultime? E come può esserci un errore di overflow aritmetico se è @big_numberpossibile memorizzare l'output di @big_number / 1?

Risposte:


18

Comprensione della precisione e della scala nel contesto delle operazioni aritmetiche

Analizziamo questo aspetto e diamo un'occhiata da vicino ai dettagli dell'operatore aritmetico di divisione . Questo è ciò che MSDN ha da dire sui tipi di risultato dell'operatore di divisione :

Tipi di risultati

Restituisce il tipo di dati dell'argomento con la precedenza più alta. Per ulteriori informazioni, vedere Precedenza dei tipi di dati (Transact-SQL) .

Se un dividendo intero viene diviso per un divisore intero, il risultato è un numero intero che ha una parte frazionaria del risultato troncata.

Sappiamo che @big_numberè un DECIMAL. Quale tipo di dati esegue il cast di SQL Server 1? Lo lancia a un INT. Possiamo confermarlo con l'aiuto di 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
;

Per i calci, possiamo anche sostituire il 1blocco di codice originale con un valore esplicitamente digitato come DECLARE @one INT = 1;e confermare che otteniamo gli stessi risultati.

Quindi abbiamo un DECIMALe un INT. Poiché DECIMALha una precedenza di tipo di dati superiore rispetto a INT, sappiamo che verrà eseguito il cast dell'output della nostra divisione DECIMAL.

Allora dov'è il problema?

Il problema è con la scala DECIMALdell'output. Ecco una tabella di regole su come SQL Server determina la precisione e la scala dei risultati ottenuti dalle operazioni aritmetiche:

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.

Ed ecco cosa abbiamo per le variabili in questa tabella:

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

In base al commento asterisco nella tabella sopra, la precisione massima che DECIMALpuò avere è 38 . Pertanto, la precisione dei nostri risultati viene ridotta da 49 a 38 e "la scala corrispondente viene ridotta per evitare che la parte integrale di un risultato venga troncata". Da questo commento non è chiaro come si riduca la scala, ma lo sappiamo:

Secondo la formula nella tabella, la scala minima possibile che puoi avere dopo aver diviso due DECIMALs è 6.

Quindi, finiamo con i seguenti risultati:

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.

Come questo spiega lo straripamento aritmetico

Ora la risposta è ovvia:

L'output della nostra divisione viene castato DECIMAL(38, 6)e DECIMAL(38, 6)non può contenere 10 37 .

Con ciò, possiamo costruire un'altra divisione che ha successo assicurandoci che il risultato possa adattarsi a 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;

Il risultato è:

10000000000000000000000000000000.000000

Nota i 6 zeri dopo il decimale. Possiamo confermare tipo di dati del risultato è DECIMAL(38, 6)da utilizzare SQL_VARIANT_PROPERTY()come sopra:

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
;

Una soluzione pericolosa

Quindi, come aggirare questa limitazione?

Bene, questo dipende certamente da ciò per cui stai facendo questi calcoli. Una soluzione a cui puoi immediatamente saltare è convertire i tuoi numeri in FLOATper i calcoli e poi riconvertirli DECIMALquando hai finito.

Ciò può funzionare in alcune circostanze, ma dovresti stare attento a capire quali siano tali circostanze. Come tutti sappiamo, la conversione di numeri da e verso FLOATè pericolosa e può fornire risultati imprevisti o errati.

Nel nostro caso, la conversione di 10 37 da e verso FLOATottiene un risultato che è semplicemente sbagliato :

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
;

E il gioco è fatto. Dividi attentamente, figli miei.



2
RE: "Modo più pulito". Potresti voler dare un'occhiata aSQL_VARIANT_PROPERTY
Martin Smith l'

@Martin - Potresti fornire un esempio o una rapida spiegazione di come potrei usare SQL_VARIANT_PROPERTYper eseguire divisioni come quella discussa nella domanda?
Nick Chammas,

1
C'è un esempio qui (in alternativa alla creazione di una nuova tabella per determinare il tipo di dati)
Martin Smith,

@Martin - Ah sì, è molto più ordinato!
Nick Chammas,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.