Tengo algunos clientes que reciben facturas extrañas. Pude aislar el problema central:
SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 200 what the?
SELECT 199.96 - (0.0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96)) -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96)) -- 199.96
-- It gets weirder...
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 0
SELECT (0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4)))) -- 0
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96)) -- 0
-- so... ... 199.06 - 0 equals 200... ... right???
SELECT 199.96 - 0 -- 199.96 ...NO....
¿Alguien tiene idea de qué diablos está pasando aquí? Quiero decir, ciertamente tiene algo que ver con el tipo de datos decimal, pero realmente no puedo entenderlo ...
Hubo mucha confusión sobre qué tipo de datos eran los números literales, así que decidí mostrar la línea real:
PS.SharePrice - (CAST((@InstallmentCount - 1) AS DECIMAL(19, 4)) * CAST(FLOOR(@InstallmentPercent * PS.SharePrice) AS DECIMAL(19, 4))))
PS.SharePrice DECIMAL(19, 4)
@InstallmentCount INT
@InstallmentPercent DECIMAL(19, 4)
Me aseguré de que el resultado de cada operación tenga un operando de un tipo diferente al que DECIMAL(19, 4)
se lanza explícitamente antes de aplicarlo al contexto externo.
Sin embargo, el resultado permanece 200.00
.
Ahora he creado una muestra resumida que pueden ejecutar en su computadora.
DECLARE @InstallmentIndex INT = 1
DECLARE @InstallmentCount INT = 1
DECLARE @InstallmentPercent DECIMAL(19, 4) = 1.0
DECLARE @PS TABLE (SharePrice DECIMAL(19, 4))
INSERT INTO @PS (SharePrice) VALUES (599.96)
-- 2000
SELECT
IIF(@InstallmentIndex < @InstallmentCount,
FLOOR(@InstallmentPercent * PS.SharePrice),
1999.96)
FROM @PS PS
-- 2000
SELECT
IIF(@InstallmentIndex < @InstallmentCount,
FLOOR(@InstallmentPercent * CAST(599.96 AS DECIMAL(19, 4))),
1999.96)
FROM @PS PS
-- 1996.96
SELECT
IIF(@InstallmentIndex < @InstallmentCount,
FLOOR(@InstallmentPercent * 599.96),
1999.96)
FROM @PS PS
-- Funny enough - with this sample explicitly converting EVERYTHING to DECIMAL(19, 4) - it still doesn't work...
-- 2000
SELECT
IIF(@InstallmentIndex < @InstallmentCount,
FLOOR(@InstallmentPercent * CAST(199.96 AS DECIMAL(19, 4))),
CAST(1999.96 AS DECIMAL(19, 4)))
FROM @PS PS
Ahora tengo algo ...
-- 2000
SELECT
IIF(1 = 2,
FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))),
CAST(1999.96 AS DECIMAL(19, 4)))
-- 1999.9600
SELECT
IIF(1 = 2,
CAST(FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) AS INT),
CAST(1999.96 AS DECIMAL(19, 4)))
Qué demonios, se supone que floor devuelve un número entero de todos modos. ¿Que está pasando aqui? :-RE
Creo que ahora me las arreglé para reducirlo a la esencia misma :-D
-- 1.96
SELECT IIF(1 = 2,
CAST(1.0 AS DECIMAL (36, 0)),
CAST(1.96 AS DECIMAL(19, 4))
)
-- 2.0
SELECT IIF(1 = 2,
CAST(1.0 AS DECIMAL (37, 0)),
CAST(1.96 AS DECIMAL(19, 4))
)
-- 2
SELECT IIF(1 = 2,
CAST(1.0 AS DECIMAL (38, 0)),
CAST(1.96 AS DECIMAL(19, 4))
)
fuente
float
Floor()
no no devolver unaint
. Devuelve el mismo tipo que la expresión original , con la parte decimal eliminada. Para el resto, laIIF()
función da como resultado el tipo con mayor prioridad ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Entonces, la segunda muestra en la que se lanza a int, la prioridad más alta es la conversión simple como numérica (19,4).float
tipos ing puntos a la moneda mango .Respuestas:
Necesito comenzar desenvolviendo esto un poco para poder ver qué está pasando:
SELECT 199.96 - ( 0.0 * FLOOR( CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)) ) )
Ahora veamos exactamente qué tipos usa SQL Server para cada lado de la operación de resta:
SELECT SQL_VARIANT_PROPERTY (199.96 ,'BaseType'), SQL_VARIANT_PROPERTY (199.96 ,'Precision'), SQL_VARIANT_PROPERTY (199.96 ,'Scale') SELECT SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) ,'BaseType'), SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) ,'Precision'), SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) ,'Scale')
Resultados:
Así
199.96
esnumeric(5,2)
y más largoFloor(Cast(etc))
esnumeric(38,1)
.Las reglas para la precisión y la escala resultantes de una operación de resta (es decir:) se
e1 - e2
ven así:Eso evalúa así:
También puede usar el enlace de reglas para averiguar de dónde
numeric(38,1)
provienen en primer lugar (pista: multiplicó dos valores de precisión 19).Pero:
¡Ups! La precisión es 40. Tenemos que reducirla, y dado que al reducir la precisión siempre se deben cortar los dígitos menos significativos, eso significa reducir la escala también. El tipo resultante final para la expresión será
numeric(38,0)
, que para199.96
redondeos a200
.Probablemente pueda solucionar este problema moviendo y consolidando las
CAST()
operaciones desde el interior de la expresión grande a unaCAST()
alrededor del resultado de la expresión completa. Así que esto:SELECT 199.96 - ( 0.0 * FLOOR( CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)) ) )
Se convierte en:
SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))
Incluso podría quitarme el yeso exterior también.
Aprendemos aquí que debemos elegir tipos que coincidan con la precisión y la escala que tenemos en este momento , en lugar del resultado esperado. No tiene sentido optar por números de gran precisión, porque SQL Server mutará esos tipos durante las operaciones aritméticas para tratar de evitar desbordamientos.
Más información:
Sql_Variant_Property()
fuente
Esté atento a los tipos de datos involucrados para la siguiente declaración:
SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))))
NUMERIC(19, 4) * NUMERIC(19, 4)
esNUMERIC(38, 7)
(ver más abajo)FLOOR(NUMERIC(38, 7))
esNUMERIC(38, 0)
(ver más abajo)0.0
esNUMERIC(1, 1)
NUMERIC(1, 1) * NUMERIC(38, 0)
esNUMERIC(38, 1)
199.96
esNUMERIC(5, 2)
NUMERIC(5, 2) - NUMERIC(38, 1)
esNUMERIC(38, 1)
(ver más abajo)Esto explica por qué termina con
200.0
( un dígito después del decimal, no cero ) en lugar de199.96
.Notas:
FLOOR
devuelve el mayor entero menor o igual que la expresión numérica especificada y el resultado tiene el mismo tipo que la entrada. Devuelve INT para INT, FLOAT para FLOAT y NUMERIC (x, 0) para NUMERIC (x, y).Según el algoritmo :
La descripción también contiene los detalles de cómo se reduce exactamente la escala dentro de las operaciones de suma y multiplicación. Basado en esa descripción:
NUMERIC(19, 4) * NUMERIC(19, 4)
esNUMERIC(39, 8)
y sujeto aNUMERIC(38, 7)
NUMERIC(1, 1) * NUMERIC(38, 0)
esNUMERIC(40, 1)
y sujeto aNUMERIC(38, 1)
NUMERIC(5, 2) - NUMERIC(38, 1)
esNUMERIC(40, 2)
y sujeto aNUMERIC(38, 1)
Aquí está mi intento de implementar el algoritmo en JavaScript. He verificado los resultados con SQL Server. Responde a la parte esencial de tu pregunta.
// https://docs.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql?view=sql-server-2017 function numericTest_mul(p1, s1, p2, s2) { // e1 * e2 var precision = p1 + p2 + 1; var scale = s1 + s2; // see notes in the linked article about multiplication operations var newscale; if (precision - scale < 32) { newscale = Math.min(scale, 38 - (precision - scale)); } else if (scale < 6 && precision - scale > 32) { newscale = scale; } else if (scale > 6 && precision - scale > 32) { newscale = 6; } console.log("NUMERIC(%d, %d) * NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale); } function numericTest_add(p1, s1, p2, s2) { // e1 + e2 var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2) + 1; var scale = Math.max(s1, s2); // see notes in the linked article about addition operations var newscale; if (Math.max(p1 - s1, p2 - s2) > Math.min(38, precision) - scale) { newscale = Math.min(precision, 38) - Math.max(p1 - s1, p2 - s2); } else { newscale = scale; } console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale); } function numericTest_union(p1, s1, p2, s2) { // e1 UNION e2 var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2); var scale = Math.max(s1, s2); // my idea of how newscale should be calculated, not official var newscale; if (precision > 38) { newscale = scale - (precision - 38); } else { newscale = scale; } console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale); } /* * first example in question */ // CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)) numericTest_mul(19, 4, 19, 4); // 0.0 * FLOOR(...) numericTest_mul(1, 1, 38, 0); // 199.96 * ... numericTest_add(5, 2, 38, 1); /* * IIF examples in question * the logic used to determine result data type of IIF / CASE statement * is same as the logic used inside UNION operations */ // FLOOR(DECIMAL(38, 7)) UNION CAST(1999.96 AS DECIMAL(19, 4))) numericTest_union(38, 0, 19, 4); // CAST(1.0 AS DECIMAL (36, 0)) UNION CAST(1.96 AS DECIMAL(19, 4)) numericTest_union(36, 0, 19, 4); // CAST(1.0 AS DECIMAL (37, 0)) UNION CAST(1.96 AS DECIMAL(19, 4)) numericTest_union(37, 0, 19, 4); // CAST(1.0 AS DECIMAL (38, 0)) UNION CAST(1.96 AS DECIMAL(19, 4)) numericTest_union(38, 0, 19, 4);
fuente