Perché 199.96 - 0 = 200 in SQL?


84

Alcuni clienti ricevono bollette strane. Sono stato in grado di isolare il problema principale:

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....

Qualcuno ha idea di cosa diavolo sta succedendo qui? Voglio dire, ha sicuramente qualcosa a che fare con il tipo di dati decimale, ma non riesco davvero a capirlo ...


C'era molta confusione su quale tipo di dati fossero i numeri letterali, quindi ho deciso di mostrare la linea reale:

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)

Mi sono assicurato che il risultato di ogni operazione con un operando di un tipo diverso da quello DECIMAL(19, 4)fosse espressamente cast prima di applicarlo al contesto esterno.

Tuttavia, il risultato rimane 200.00.


Ora ho creato un campione ridotto che voi ragazzi potete eseguire sul vostro computer.

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

Adesso ho qualcosa ...

-- 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)))

Che diavolo - il pavimento dovrebbe comunque restituire un numero intero. Cosa sta succedendo qui? MrGreen


Penso di essere riuscito a ridurlo davvero all'essenza stessa MrGreen

-- 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))
)

4
@Sliverdust 199.96 -0 non è uguale a 200. Tutti quei cast e pavimenti con conversioni implicite in virgola mobile e viceversa sono garantiti per provocare una perdita di precisione.
Panagiotis Kanavos

1
@ Silverdust solo se proveniva da un tavolo. Come letterale in un'espressione è probabilmente unfloat
Panagiotis Kanavos

1
Oh ... e Floor()non senza restituire un int. Restituisce lo stesso tipo dell'espressione originale , con la parte decimale rimossa. Per il resto, la IIF()funzione restituisce il tipo con la precedenza più alta ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Quindi il secondo esempio in cui esegui il cast a int, la precedenza più alta è il cast semplice come numerico (19,4).
Joel Coehoorn

1
Ottima risposta (chi sapeva che potresti esaminare i metadati di una variante sql?) Ma nel 2012 ottengo i risultati attesi (199.96).
benjamin moskovits

2
Non ho molta familiarità con MS SQL, ma devo dire che guardare tutte quelle operazioni di cast e così via ha attirato rapidamente la mia attenzione .. quindi devo collegarlo perché nessuno dovrebbe mai usare floati tipi di punto di ing per gestire la valuta .
code_dredd

Risposte:


78

Devo iniziare scartando un po 'questo in modo da poter vedere cosa sta succedendo:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

Ora vediamo esattamente quali tipi utilizza SQL Server per ogni lato dell'operazione di sottrazione:

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')

Risultati:

numerico 5 2
numerico 38 1

Così 199.96è numeric(5,2)e più Floor(Cast(etc))è lungo numeric(38,1).

Le regole per la precisione e la scala risultanti di un'operazione di sottrazione (es . e1 - e2:) hanno questo aspetto:

Precisione: max (s1, s2) + max (p1-s1, p2-s2) + 1
Scala: max (s1, s2)

Che valuta in questo modo:

Precisione: max (1,2) + max (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
Scala: max (1,2) => 2

Puoi anche utilizzare il collegamento alle regole per capire da dove numeric(38,1)proviene (suggerimento: hai moltiplicato due valori di precisione 19).

Ma:

  • La precisione e la scala del risultato hanno un massimo assoluto di 38. Quando la precisione del risultato è maggiore di 38, viene ridotta a 38 e la scala corrispondente viene ridotta per cercare di impedire che la parte integrante di un risultato venga troncata. In alcuni casi come la moltiplicazione o la divisione, il fattore di scala non verrà ridotto per mantenere la precisione decimale, sebbene l'errore di overflow possa essere aumentato.

Ops. La precisione è 40. Dobbiamo ridurla, e poiché ridurre la precisione dovrebbe sempre tagliare le cifre meno significative, significa anche ridurre la scala. Il tipo finale risultante per l'espressione sarà numeric(38,0), which for 199.96rounds to 200.

Probabilmente puoi risolvere questo problema spostando e consolidando le CAST()operazioni dall'interno dell'espressione grande a una CAST() attorno all'intero risultato dell'espressione. Così questo:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

Diventa:

SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))

Potrei persino rimuovere anche il cast esterno.

Impariamo qui che dovremmo scegliere i tipi per abbinare la precisione e la scala che abbiamo effettivamente in questo momento , piuttosto che il risultato atteso. Non ha senso optare solo per numeri di grande precisione, perché SQL Server modificherà questi tipi durante le operazioni aritmetiche per cercare di evitare overflow.


Maggiori informazioni:


20

Tieni d'occhio i tipi di dati coinvolti per la seguente dichiarazione:

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)è NUMERIC(38, 7)(vedi sotto)
    • FLOOR(NUMERIC(38, 7))è NUMERIC(38, 0)(vedi sotto)
  2. 0.0 è NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0) è NUMERIC(38, 1)
  3. 199.96 è NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)è NUMERIC(38, 1)(vedi sotto)

Questo spiega perché si finisce con 200.0( una cifra dopo il decimale, non zero ) invece di 199.96.

Appunti:

FLOORrestituisce il numero intero più grande minore o uguale all'espressione numerica specificata e il risultato ha lo stesso tipo dell'input. Restituisce INT per INT, FLOAT per FLOAT e NUMERIC (x, 0) per NUMERIC (x, y).

Secondo l'algoritmo :

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)

* La precisione e la scala del risultato hanno un massimo assoluto di 38. Quando la precisione del risultato è maggiore di 38, viene ridotta a 38 e la scala corrispondente viene ridotta per cercare di evitare che la parte integrante di un risultato venga troncata.

La descrizione contiene anche i dettagli di come esattamente la scala viene ridotta all'interno delle operazioni di addizione e moltiplicazione. Sulla base di quella descrizione:

  • NUMERIC(19, 4) * NUMERIC(19, 4)è NUMERIC(39, 8)e bloccato aNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)è NUMERIC(40, 1)e bloccato aNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)è NUMERIC(40, 2)e bloccato aNUMERIC(38, 1)

Ecco il mio tentativo di implementare l'algoritmo in JavaScript. Ho incrociato i risultati con SQL Server. Risponde alla parte essenziale della tua domanda.

// 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);

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.