Comprensión de precisión y escala en el contexto de operaciones aritméticas
Analicemos esto y observemos de cerca los detalles del operador aritmético de división . Esto es lo que MSDN tiene que decir sobre los tipos de resultados del operador de división :
Tipos de resultados
Devuelve el tipo de datos del argumento con mayor precedencia. Para obtener más información, vea Precedencia de tipo de datos (Transact-SQL) .
Si un dividendo entero se divide por un divisor entero, el resultado es un entero que tiene cualquier parte fraccionaria del resultado truncado.
Sabemos que @big_number
es un DECIMAL
. ¿Qué tipo de datos convierte SQL Server 1
? Lo lanza a un INT
. Podemos confirmar esto con la ayuda 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 las patadas, también podemos reemplazar el 1
bloque de código original con un valor explícitamente escrito como DECLARE @one INT = 1;
y confirmar que obtenemos los mismos resultados.
Entonces tenemos una DECIMAL
y una INT
. Dado que DECIMAL
tiene una precedencia de tipo de datos más alta que INT
, sabemos que la salida de nuestra división se convertirá en DECIMAL
.
Entonces, ¿dónde está el problema?
El problema es con la escala de DECIMAL
en la salida. Aquí hay una tabla de reglas sobre cómo SQL Server determina la precisión y la escala de los resultados obtenidos de las operaciones 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.
Y esto es lo que tenemos para las variables en esta tabla:
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
Según el comentario de asterisco en la tabla anterior, la precisión máxima que DECIMAL
puede tener es 38 . Por lo tanto, la precisión de nuestros resultados se reduce de 49 a 38, y "la escala correspondiente se reduce para evitar que la parte integral de un resultado se trunca". No está claro en este comentario cómo se reduce la escala, pero sí sabemos esto:
Según la fórmula de la tabla, la escala mínima posible que puede tener después de dividir dos DECIMAL
s es 6.
Por lo tanto, terminamos con los siguientes 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.
Cómo esto explica el desbordamiento aritmético
Ahora la respuesta es obvia:
La salida de nuestra división se emite DECIMAL(38, 6)
y DECIMAL(38, 6)
no puede contener 10 37 .
Con eso, podemos construir otra división que tenga éxito al asegurarnos de que el resultado pueda encajar 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;
El resultado es:
10000000000000000000000000000000.000000
Tenga en cuenta los 6 ceros después del decimal. Podemos confirmar que el tipo de datos del resultado es DECIMAL(38, 6)
mediante el uso SQL_VARIANT_PROPERTY()
anterior:
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 solución peligrosa
Entonces, ¿cómo podemos superar esta limitación?
Bueno, eso ciertamente depende de para qué estás haciendo estos cálculos. Una solución a la que puede saltar de inmediato es convertir sus números FLOAT
para los cálculos y luego volver a convertirlos DECIMAL
cuando haya terminado.
Eso puede funcionar en algunas circunstancias, pero debe tener cuidado de comprender cuáles son esas circunstancias. Como todos sabemos, convertir números hacia y desde FLOAT
es peligroso y puede dar resultados inesperados o incorrectos.
En nuestro caso, la conversión de 10 37FLOAT
ay desde obtiene un resultado que es simplemente incorrecto :
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
;
Y ahí lo tienes. Divide con cuidado, hijos míos.
SQL_VARIANT_PROPERTY
SQL_VARIANT_PROPERTY
para realizar divisiones como la que se discutió en la pregunta?