Niektórzy klienci otrzymują dziwne rachunki. Udało mi się wyodrębnić podstawowy problem:
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....
Czy ktoś ma wskazówkę, co tu się dzieje? Chodzi mi o to, że z pewnością ma to coś wspólnego z typem danych dziesiętnych, ale nie mogę się tym zająć ...
Było wiele nieporozumień co do typu danych, które były literały liczbowe, więc zdecydowałem się pokazać prawdziwą linię:
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)
Upewniłem się, że wynik każdej operacji mającej operand innego typu niż DECIMAL(19, 4)
jest rzutowany jawnie przed zastosowaniem go do kontekstu zewnętrznego.
Niemniej jednak wynik pozostaje 200.00
.
Stworzyłem teraz skróconą próbkę, którą możecie wykonać na swoim komputerze.
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
Teraz mam coś ...
-- 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)))
Co do diabła - podłoga i tak powinna zwrócić liczbę całkowitą. Co tu się dzieje? :-RE
Myślę, że teraz udało mi się naprawdę sprowadzić to do samej istoty :-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))
)
źródło
float
Floor()
nie nie zwracaćint
. Zwraca ten sam typ, co oryginalne wyrażenie , z usuniętą częścią dziesiętną. W pozostałej częściIIF()
funkcja zwraca typ o najwyższym priorytecie ( docs.microsoft.com/en-us/sql/t-sql/functions/ ... ). Zatem w drugiej próbce, w której rzutujesz na int, wyższy priorytet ma rzutowanie proste jako numeryczne (19,4).float
typów punktów wejścia do obsługi waluty .Odpowiedzi:
Muszę trochę odpakować, żeby zobaczyć, co się dzieje:
SELECT 199.96 - ( 0.0 * FLOOR( CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)) ) )
Zobaczmy teraz dokładnie, jakich typów SQL Server używa dla każdej strony operacji odejmowania:
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')
Wyniki:
Tak
199.96
jestnumeric(5,2)
i im dłużejFloor(Cast(etc))
jestnumeric(38,1)
.Te zasady wynikającej precyzji i skali operacji odejmowania (tj:
e1 - e2
) wygląda następująco:To ocenia następująco:
Możesz również skorzystać z linku do reguł, aby dowiedzieć się, skąd
numeric(38,1)
pochodzi wartość (wskazówka: pomnożyłeś dwie wartości z dokładnością do 19).Ale:
Ups. Precyzja wynosi 40. Musimy ją zmniejszyć, a ponieważ zmniejszenie precyzji powinno zawsze odcinać najmniej znaczące cyfry, oznacza to również zmniejszenie skali. Ostatnim typem wynikowym dla wyrażenia będzie
numeric(38,0)
, który dla199.96
rund do200
.Prawdopodobnie można to naprawić, przenosząc i konsolidując
CAST()
operacje z wnętrza dużego wyrażenia do jednegoCAST()
wokół całego wyniku wyrażenia. Więc to:SELECT 199.96 - ( 0.0 * FLOOR( CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)) ) )
Staje się:
SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))
Mógłbym nawet usunąć zewnętrzny odlew.
Uczymy się tutaj powinniśmy wybrać typy pasujące do precyzji i skali rzeczywiście mają teraz , zamiast oczekiwanego wyniku. Nie ma sensu po prostu wybierać liczb o dużej precyzji, ponieważ SQL Server zmutuje te typy podczas operacji arytmetycznych, aby spróbować uniknąć przepełnienia.
Więcej informacji:
Sql_Variant_Property()
źródło
Zwróć uwagę na typy danych związane z następującym stwierdzeniem:
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)
jestNUMERIC(38, 7)
(patrz poniżej)FLOOR(NUMERIC(38, 7))
jestNUMERIC(38, 0)
(patrz poniżej)0.0
jestNUMERIC(1, 1)
NUMERIC(1, 1) * NUMERIC(38, 0)
jestNUMERIC(38, 1)
199.96
jestNUMERIC(5, 2)
NUMERIC(5, 2) - NUMERIC(38, 1)
jestNUMERIC(38, 1)
(patrz poniżej)To wyjaśnia, dlaczego otrzymujesz
200.0
( jedna cyfra po przecinku, a nie zero ) zamiast199.96
.Uwagi:
FLOOR
zwraca największą liczbę całkowitą mniejszą lub równą podanemu wyrażeniu liczbowemu, a wynik ma taki sam typ jak dane wejściowe. Zwraca INT dla INT, FLOAT dla FLOAT i NUMERIC (x, 0) dla NUMERIC (x, y).Zgodnie z algorytmem :
Opis zawiera również szczegółowe informacje o tym, jak dokładnie skala jest zmniejszana w ramach operacji dodawania i mnożenia. Na podstawie tego opisu:
NUMERIC(19, 4) * NUMERIC(19, 4)
jestNUMERIC(39, 8)
i jest zamocowany doNUMERIC(38, 7)
NUMERIC(1, 1) * NUMERIC(38, 0)
jestNUMERIC(40, 1)
i jest zamocowany doNUMERIC(38, 1)
NUMERIC(5, 2) - NUMERIC(38, 1)
jestNUMERIC(40, 2)
i jest zamocowany doNUMERIC(38, 1)
Oto moja próba zaimplementowania algorytmu w JavaScript. Sprawdziłem wyniki z SQL Server. Odpowiada na istotną część twojego pytania.
// 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);
źródło