Warum ist 199,96 - 0 = 200 in SQL?

84

Ich habe einige Kunden, die seltsame Rechnungen bekommen. Ich konnte das Kernproblem eingrenzen:

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

Hat jemand eine Ahnung, was zum Teufel hier passiert? Ich meine, es hat sicherlich etwas mit dem dezimalen Datentyp zu tun, aber ich kann mich nicht wirklich darum kümmern ...


Es gab viel Verwirrung darüber, welcher Datentyp die Zahlenliterale waren, also beschloss ich, die reale Linie zu zeigen:

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)

Ich habe dafür gesorgt, dass das Ergebnis jeder Operation einen Operanden eines anderen Typs als hat DECIMAL(19, 4) explizit umgewandelt hat, bevor ihn auf den äußeren Kontext anwende.

Trotzdem bleibt das Ergebnis 200.00 .


Ich habe jetzt ein Beispiel erstellt, das ihr auf eurem Computer ausführen könnt.

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

Jetzt habe ich etwas ...

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

Was zum Teufel - Boden soll sowieso eine ganze Zahl zurückgeben. Was ist denn hier los? :-D


Ich glaube, ich habe es jetzt wirklich geschafft, es auf das Wesentliche zu reduzieren :-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))
)
Silberstaub
quelle
4
@Sliverdust 199.96 -0 entspricht nicht 200. Alle diese Abgüsse und Böden mit impliziten Konvertierungen in Gleitkomma und zurück führen jedoch garantiert zu Präzisionsverlusten.
Panagiotis Kanavos
1
@Silverdust nur wenn es von einem Tisch kam. Als wörtliches float
Wort
1
Oh ... und Floor()kommt nicht zurück int. Es gibt den gleichen Typ wie der ursprüngliche Ausdruck zurück , wobei der Dezimalteil entfernt wird. Im Übrigen IIF()ergibt die Funktion den Typ mit der höchsten Priorität ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Bei der zweiten Stichprobe, bei der Sie in int umwandeln, ist die einfache Umwandlung als numerisch (19,4) die höhere Priorität.
Joel Coehoorn
1
Tolle Antwort (wer wusste, dass Sie Metadaten einer SQL-Variante untersuchen können?), Aber 2012 erhalte ich die erwarteten Ergebnisse (199,96).
Benjamin Moskovits
2
Ich bin nicht allzu vertraut mit MS SQL, aber ich muss sagen , dass bei all diesen Guss Operationen suchen und so weiter schnell meine Aufmerksamkeit erregt .. also muss ich den Link an, weil niemand sollte jemals verwenden floating Punkttypen Griff Währung .
code_dredd

Antworten:

78

Ich muss damit beginnen, dies ein wenig auszupacken, damit ich sehen kann, was los ist:

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

Lassen Sie uns nun genau sehen, welche Typen SQL Server für jede Seite der Subtraktionsoperation verwendet:

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

Ergebnisse:

numerisch 5 2
numerisch 38 1

So 199.96ist numeric(5,2)und je länger Floor(Cast(etc))ist numeric(38,1).

Die Regeln für die resultierende Genauigkeit und Skalierung einer Subtraktionsoperation (dh :) e1 - e2sehen folgendermaßen aus:

Präzision: max (s1, s2) + max (p1-s1, p2-s2) + 1
Maßstab: max (s1, s2)

Das bewertet sich so:

Präzision: max (1,2) + max (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
Maßstab: max (1,2) => 2

Sie können auch den Link Regeln verwenden, um herauszufinden, woher die numeric(38,1)ursprünglich kamen (Hinweis: Sie haben zwei Werte mit einer Genauigkeit von 19 multipliziert).

Aber:

  • Die Ergebnisgenauigkeit und Skalierung haben ein absolutes Maximum von 38. Wenn eine Ergebnisgenauigkeit größer als 38 ist, wird sie auf 38 reduziert, und die entsprechende Skalierung wird reduziert, um zu verhindern, dass der integrale Teil eines Ergebnisses abgeschnitten wird. In einigen Fällen wie Multiplikation oder Division wird der Skalierungsfaktor nicht reduziert, um die Dezimalgenauigkeit beizubehalten, obwohl der Überlauffehler erhöht werden kann.

Hoppla. Die Genauigkeit beträgt 40. Wir müssen sie reduzieren, und da die Reduzierung der Genauigkeit immer die niedrigstwertigen Stellen abschneiden sollte, bedeutet dies auch eine Reduzierung der Skalierung. Der endgültige resultierende Typ für den Ausdruck ist numeric(38,0), welcher für 199.96Runden zu 200.

Sie können dies wahrscheinlich beheben, indem Sie die CAST()Operationen aus dem großen Ausdruck heraus in eine CAST() um das gesamte Ausdrucksergebnis verschieben und konsolidieren . Also das:

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

Wird:

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

Ich könnte sogar den äußeren Gips entfernen.

Wir lernen hier sollten wir Typen wählen , die Präzision anzupassen und skalieren wir tatsächlich haben gerade jetzt , und nicht das erwartete Ergebnis. Es ist nicht sinnvoll, nur Zahlen mit hoher Genauigkeit zu verwenden, da SQL Server diese Typen während arithmetischer Operationen mutiert, um Überläufe zu vermeiden.


Mehr Informationen:

Stanislav Kundii
quelle
20

Behalten Sie die beteiligten Datentypen für die folgende Aussage im Auge:

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

Dies erklärt, warum Sie am Ende 200.0( eine Ziffer nach der Dezimalstelle, nicht Null ) anstelle von erhalten 199.96.

Anmerkungen:

FLOORGibt die größte Ganzzahl zurück, die kleiner oder gleich dem angegebenen numerischen Ausdruck ist, und das Ergebnis hat den gleichen Typ wie die Eingabe. Es gibt INT für INT, FLOAT für FLOAT und NUMERIC (x, 0) für NUMERIC (x, y) zurück.

Nach dem Algorithmus :

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)

* Die Ergebnisgenauigkeit und Skalierung haben ein absolutes Maximum von 38. Wenn eine Ergebnisgenauigkeit größer als 38 ist, wird sie auf 38 reduziert, und die entsprechende Skalierung wird reduziert, um zu verhindern, dass der integrale Teil eines Ergebnisses abgeschnitten wird.

Die Beschreibung enthält auch Einzelheiten darüber, wie genau die Skala innerhalb von Additions- und Multiplikationsoperationen reduziert wird. Basierend auf dieser Beschreibung:

  • NUMERIC(19, 4) * NUMERIC(19, 4)ist NUMERIC(39, 8)und festgeklemmtNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)ist NUMERIC(40, 1)und festgeklemmtNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)ist NUMERIC(40, 2)und festgeklemmtNUMERIC(38, 1)

Hier ist mein Versuch, den Algorithmus in JavaScript zu implementieren. Ich habe die Ergebnisse mit SQL Server abgeglichen. Es beantwortet den wesentlichen Teil Ihrer Frage.

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