it-swarm.com.de

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

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 eine Menge Verwirrung darüber, welcher Datentyp die Zahlenliterale waren, deshalb habe ich beschlossen, 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 sichergestellt, dass das Ergebnis jeder Operation, die einen anderen Operanden als DECIMAL(19, 4) enthält, explizit umgewandelt wird, bevor es auf den äußeren Kontext angewendet wird.

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 soll sowieso eine ganze Zahl zurückgeben. Was ist 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))
)
83
Silverdust

Ich muss dies zunächst ein wenig auspacken, 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 

Also ist 199.96numeric(5,2) und die längere Floor(Cast(etc)) ist numeric(38,1).

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

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

Das wertet sich so aus:

Genauigkeit: 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 rules verwenden, um herauszufinden, woher numeric(38,1) stammt (Hinweis: Sie haben zwei Werte mit der Genauigkeit 19 multipliziert).

Aber:

  • Die Ergebnisgenauigkeit und der Maßstab haben ein absolutes Maximum von 38. Wenn die Ergebnisgenauigkeit größer als 38 ist, wird sie auf 38 verringert, und der entsprechende Maßstab wird verringert, 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 ist 40. Wir müssen sie verringern, und da die Verringerung der Genauigkeit immer die niedrigstwertigen Stellen abschneiden sollte, bedeutet dies auch, dass die Skalierung verringert wird. Der endgültige resultierende Typ für den Ausdruck ist numeric(38,0), der für 199.96 Auf 200 Gerundet wird.

Sie können dies wahrscheinlich beheben, indem Sie die CAST() -Operationen aus dem großen Ausdruck in oneCAST() 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, dass wir Typen auswählen sollten, die der Genauigkeit und dem Maßstab entsprechen, die wir tatsächlich haben im Moment, und nicht dem erwarteten Ergebnis. Es ist nicht sinnvoll, nur große Präzisionszahlen anzustreben, da SQL Server diese Typen bei arithmetischen Operationen mutiert, um Überläufe zu vermeiden.


Mehr Informationen:

77

Behalten Sie die beteiligten Datentypen im Auge, um folgende Aussage zu erhalten:

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 stattdessen 200.0 ( eine Stelle nach dem Komma, nicht Null) erhalten von 199.96.

Anmerkungen:

FLOOR gibt 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 der Maßstab haben ein absolutes Maximum von 38. Wenn die Ergebnisgenauigkeit größer als 38 ist, wird sie auf 38 reduziert und der entsprechende Maßstab 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 wird an NUMERIC(38, 7) geklemmt
  • NUMERIC(1, 1) * NUMERIC(38, 0) ist NUMERIC(40, 1) und wird an NUMERIC(38, 1) geklemmt
  • NUMERIC(5, 2) - NUMERIC(38, 1) ist NUMERIC(40, 2) und wird an NUMERIC(38, 1) geklemmt

Hier ist mein Versuch, den Algorithmus in JavaScript zu implementieren. Ich habe die Ergebnisse mit SQL Server abgeglichen. Es beantwortet den sehr 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);
20
Salman A