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 1
blocco di codice originale con un valore esplicitamente digitato come DECLARE @one INT = 1;
e confermare che otteniamo gli stessi risultati.
Quindi abbiamo un DECIMAL
e un INT
. Poiché DECIMAL
ha 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 DECIMAL
dell'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 DECIMAL
può 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 DECIMAL
s è 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 FLOAT
per i calcoli e poi riconvertirli DECIMAL
quando 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 FLOAT
ottiene 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.