У меня есть клиенты, которые получают странные счета. Мне удалось выделить основную проблему:
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....
Кто-нибудь знает, что, черт возьми, здесь происходит? Я имею в виду, что это определенно как-то связано с типом данных decimal, но я не могу осмыслить это ...
Было много недоразумений относительно того, к какому типу данных относятся числовые литералы, поэтому я решил показать реальную строку:
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)
Я убедился, что результат каждой операции, имеющей тип операнда, отличный от DECIMAL(19, 4)
приведенного явно, перед применением его к внешнему контексту.
Тем не менее результат остается 200.00
.
Теперь я создал упрощенный образец, который вы, ребята, можете выполнить на своем компьютере.
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
Теперь у меня есть кое-что ...
-- 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)))
Какого черта - floor все равно должен возвращать целое число. Что тут происходит? :-D
Думаю, теперь мне удалось по-настоящему свести это к самой сути :-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))
)
источник
float
Floor()
вовсе не возвращатьint
. Он возвращает тот же тип, что и исходное выражение , с удаленной десятичной частью. В остальномIIF()
функция возвращает тип с наивысшим приоритетом ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Итак, во втором примере, где вы приводите к int, более высокий приоритет - это простое приведение к числовому (19,4).float
типы точек входа для обработки валюты .Ответы:
Мне нужно начать с небольшого разворачивания этого файла, чтобы я мог видеть, что происходит:
SELECT 199.96 - ( 0.0 * FLOOR( CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)) ) )
Теперь давайте посмотрим, какие типы SQL Server использует для каждой стороны операции вычитания:
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')
Полученные результаты:
Так
199.96
этоnumeric(5,2)
и чем дольшеFloor(Cast(etc))
находитсяnumeric(38,1)
.Эти правила результирующей точности и масштаба операции вычитания (т.е.
e1 - e2
) выглядеть следующим образом :Это оценивается так:
Вы также можете использовать ссылку на правила, чтобы выяснить, откуда оно
numeric(38,1)
взялось (подсказка: вы умножили два значения точности 19).Но:
Ой. Точность равна 40. Мы должны ее уменьшить, и поскольку уменьшение точности всегда должно отсекать наименее значащие цифры, это также означает уменьшение масштаба. Конечным результирующим типом для выражения будет
numeric(38,0)
, для199.96
округления до200
.Вероятно, вы можете исправить это, переместив и объединив
CAST()
операции внутри большого выражения в однуCAST()
вокруг всего результата выражения. Итак, это:SELECT 199.96 - ( 0.0 * FLOOR( CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)) ) )
Становится:
SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))
Я мог бы даже снять внешний слепок.
Здесь мы узнаем, что нам следует выбирать типы, которые соответствуют точности и масштабу, которые у нас есть сейчас , а не ожидаемому результату. Нет смысла просто использовать числа с большой точностью, потому что SQL Server будет изменять эти типы во время арифметических операций, чтобы избежать переполнения.
Больше информации:
Sql_Variant_Property()
источник
Следите за типами данных, используемыми для следующего оператора:
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)
естьNUMERIC(38, 7)
(см. ниже)FLOOR(NUMERIC(38, 7))
естьNUMERIC(38, 0)
(см. ниже)0.0
являетсяNUMERIC(1, 1)
NUMERIC(1, 1) * NUMERIC(38, 0)
являетсяNUMERIC(38, 1)
199.96
являетсяNUMERIC(5, 2)
NUMERIC(5, 2) - NUMERIC(38, 1)
естьNUMERIC(38, 1)
(см. ниже)Это объясняет, почему вы получаете
200.0
( одна цифра после десятичной дроби, а не ноль ) вместо199.96
.Ноты:
FLOOR
возвращает наибольшее целое число, меньшее или равное указанному числовому выражению, и результат имеет тот же тип, что и ввод. Он возвращает INT для INT, FLOAT для FLOAT и NUMERIC (x, 0) для NUMERIC (x, y).По алгоритму :
В описании также содержится подробная информация о том, как именно уменьшается масштаб в операциях сложения и умножения. Основываясь на этом описании:
NUMERIC(19, 4) * NUMERIC(19, 4)
естьNUMERIC(39, 8)
и зажатNUMERIC(38, 7)
NUMERIC(1, 1) * NUMERIC(38, 0)
естьNUMERIC(40, 1)
и зажатNUMERIC(38, 1)
NUMERIC(5, 2) - NUMERIC(38, 1)
естьNUMERIC(40, 2)
и зажатNUMERIC(38, 1)
Вот моя попытка реализовать алгоритм на JavaScript. Я перепроверил результаты с SQL Server. Он отвечает на самую суть вашего вопроса.
// 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);
источник