Entendendo precisão e escala no contexto de operações aritméticas
Vamos detalhar isso e dar uma olhada nos detalhes do operador aritmético de divisão . Isto é o que o MSDN tem a dizer sobre os tipos de resultados do operador de divisão :
Tipos de resultado
Retorna o tipo de dados do argumento com maior precedência. Para obter mais informações, consulte Precedência de tipos de dados (Transact-SQL) .
Se um dividendo inteiro é dividido por um divisor inteiro, o resultado é um número inteiro que tem qualquer parte fracionária do resultado truncada.
Sabemos que @big_number
é um DECIMAL
. Que tipo de dados o SQL Server lança 1
como? Ele lança para um INT
. Podemos confirmar isso com a ajuda 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
;
Para chutes, também podemos substituir o 1
no bloco de código original por um valor digitado explicitamente como DECLARE @one INT = 1;
e confirmar que obtemos os mesmos resultados.
Então nós temos um DECIMAL
e um INT
. Como DECIMAL
possui uma precedência de tipo de dados mais alta que INT
, sabemos que a saída de nossa divisão será convertida em DECIMAL
.
Então, onde está o problema?
O problema está na escala de DECIMAL
saída. Aqui está uma tabela de regras sobre como o SQL Server determina a precisão e a escala dos resultados obtidos nas operações aritméticas:
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.
E aqui está o que temos para as variáveis nesta tabela:
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
De acordo com o comentário do asterisco na tabela acima, a precisão máxima que uma DECIMAL
lata pode ter é 38 . Portanto, a precisão de nossos resultados é reduzida de 49 para 38 e "a escala correspondente é reduzida para impedir que a parte integrante de um resultado seja truncada". Não está claro neste comentário como a escala é reduzida, mas sabemos disso:
De acordo com a fórmula da tabela, a escala mínima possível que você pode ter depois de dividir dois DECIMAL
s é 6.
Assim, acabamos com os seguintes resultados:
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.
Como isso explica o estouro aritmético
Agora a resposta é óbvia:
A saída de nossa divisão é lançada DECIMAL(38, 6)
e DECIMAL(38, 6)
não pode conter 10 37 .
Com isso, podemos construir uma outra divisão que sucede por ter certeza que o resultado pode caber em 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;
O resultado é:
10000000000000000000000000000000.000000
Observe os 6 zeros após o decimal. Podemos confirmar tipo de dados do resultado é DECIMAL(38, 6)
usando SQL_VARIANT_PROPERTY()
como acima:
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
;
Uma solução perigosa
Então, como contornar essa limitação?
Bem, isso certamente depende do motivo pelo qual você está fazendo esses cálculos. Uma solução para a qual você pode ir imediatamente é converter seus números FLOAT
para os cálculos e depois convertê-los novamente DECIMAL
quando terminar.
Isso pode funcionar em algumas circunstâncias, mas você deve ter cuidado para entender quais são essas circunstâncias. Como todos sabemos, converter números de e para FLOAT
é perigoso e pode fornecer resultados inesperados ou incorretos.
No nosso caso, converter 10 37 para e de FLOAT
obtém um resultado totalmente errado :
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 aí está. Divida com cuidado, meus filhos.
SQL_VARIANT_PROPERTY
SQL_VARIANT_PROPERTY
para executar divisões como a discutida na pergunta?