Dlaczego 199,96 - 0 = 200 w SQL?

84

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))
)
Silverdust
źródło
4
@Sliverdust 199.96 -0 nie równa się 200. Wszystkie te rzuty i podłogi z niejawnymi konwersjami na zmiennoprzecinkowe iz powrotem gwarantują jednak utratę precyzji.
Panagiotis Kanavos
1
@Silverdust tylko wtedy, gdy pochodzi ze stołu. W dosłownym wyrażeniu jest to prawdopodobniefloat
Panagiotis Kanavos
1
Och ... i Floor()nie nie zwracać int. Zwraca ten sam typ, co oryginalne wyrażenie , z usuniętą częścią dziesiętną. W pozostałej części IIF()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).
Joel Coehoorn
1
Świetna odpowiedź (kto wiedział, że możesz sprawdzić metadane wariantu sql?), Ale w 2012 roku otrzymuję oczekiwane wyniki (199,96).
benjamin moskovits
2
Nie jestem zbyt zaznajomiony z MS SQL, ale muszę powiedzieć, że spojrzenie na wszystkie te operacje rzutowania i tak dalej szybko przykuło moją uwagę ... więc muszę to połączyć, ponieważ nikt nie powinien nigdy używać floattypów punktów wejścia do obsługi waluty .
code_dredd

Odpowiedzi:

78

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:

numeryczne 5 2
numeryczne 38 1

Tak 199.96jest numeric(5,2)i im dłużej Floor(Cast(etc))jest numeric(38,1).

Te zasady wynikającej precyzji i skali operacji odejmowania (tj: e1 - e2) wygląda następująco:

Precyzja: max (s1, s2) + max (p1-s1, p2-s2) + 1
Skala: max (s1, s2)

To ocenia następująco:

Precyzja: max (1,2) + max (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
Skala: max (1,2) => 2

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:

  • Dokładność wyniku i skala mają absolutne maksimum 38. Gdy dokładność wyniku jest większa niż 38, jest ona zmniejszana do 38, a odpowiadająca skala jest redukowana, aby zapobiec obcięciu integralnej części wyniku. W niektórych przypadkach, takich jak mnożenie lub dzielenie, współczynnik skali nie zostanie zmniejszony w celu zachowania dokładności dziesiętnej, chociaż można zwiększyć błąd przepełnienia.

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 dla 199.96rund do 200.

Prawdopodobnie można to naprawić, przenosząc i konsolidując CAST()operacje z wnętrza dużego wyrażenia do jednego CAST() 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:

Stanislav Kundii
źródło
20

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))))
  1. NUMERIC(19, 4) * NUMERIC(19, 4)jest NUMERIC(38, 7)(patrz poniżej)
    • FLOOR(NUMERIC(38, 7))jest NUMERIC(38, 0)(patrz poniżej)
  2. 0.0 jest NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0) jest NUMERIC(38, 1)
  3. 199.96 jest NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)jest NUMERIC(38, 1)(patrz poniżej)

To wyjaśnia, dlaczego otrzymujesz 200.0( jedna cyfra po przecinku, a nie zero ) zamiast 199.96.

Uwagi:

FLOORzwraca 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 :

Operation | Result precision                    | Result scale*
e1 * e2   | p1 + p2 + 1                         | s1 + s2
e1 - e2   | max(s1, s2) + max(p1-s1, p2-s2) + 1 | max(s1, s2)

* Dokładność wyniku i skala mają absolutne maksimum 38. Gdy dokładność wyniku jest większa niż 38, jest ona zmniejszana do 38, a odpowiednia skala jest redukowana, aby zapobiec obcięciu integralnej części wyniku.

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)jest NUMERIC(39, 8)i jest zamocowany doNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)jest NUMERIC(40, 1)i jest zamocowany doNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)jest NUMERIC(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);

Salman A
źródło