SQL'de neden 199.96 - 0 = 200?


84

Garip faturalar alan bazı müşterilerim var. Temel sorunu izole edebildim:

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

Burada neler olduğu hakkında bir fikriniz var mı? Demek istediğim, kesinlikle ondalık veri türü ile bir ilgisi var, ama gerçekten kafamı etrafına dolayamıyorum ...


Sayı değişmezlerinin hangi veri türü olduğu konusunda çok fazla kafa karışıklığı vardı, bu yüzden gerçek satırı göstermeye karar verdim:

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)

DECIMAL(19, 4)Dış bağlama uygulamadan önce farklı türde bir işlenene sahip her işlemin sonucunun açıkça dönüştürüldüğünden emin oldum .

Yine de sonuç kalır 200.00.


Şimdi bilgisayarınızda uygulayabileceğiniz kaynatılmış bir örnek oluşturdum.

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

Şimdi bende bir şey var ...

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

Zaten katın bir tamsayı döndürmesi gerekiyor. Burada neler oluyor? :-D


Sanırım şimdi onu gerçekten özüne indirgemeyi başardım: -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))
)

4
@Sliverdust 199.96 -0, 200'e eşit değildir. Tüm bu dökümler ve kayan noktaya ve geriye örtük dönüşümlü zeminlerin hassasiyet kaybına neden olacağı garanti edilir.
Panagiotis Kanavos

1
@Silverdust sadece bir masadan geliyorsa. Bir ifadede gerçek bir ifade olarak muhtemelen birfloat
Panagiotis Kanavos

1
Oh ... ve Floor()yok değil bir dönüş int. Ondalık kısım kaldırılarak orijinal ifadeyle aynı türü döndürür . Geri kalanı için, IIF()işlev en yüksek önceliğe sahip türle sonuçlanır ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Dolayısıyla, int'e çevirdiğiniz 2. örnek, daha yüksek öncelik, sayısal olarak basit dökümdür (19,4).
Joel Coehoorn

1
Harika yanıt (bir sql değişkeninin meta verilerini inceleyebileceğinizi kim bilebilirdi?) Ancak 2012'de beklenen sonuçları alıyorum (199,96).
benjamin moskovits

2
MS SQL ile çok aşina değilim ama gerekir bu yüzden .. benim dikkatimi çekti bu yüzden hızla bu döküm işlemlerinin tüm bakarak ve söylemeliyim bu bağlantı kimse olmalıdır, çünkü şimdiye kadar kullanıyor floatsap para birimine ing noktalı tiplerini .
code_dredd

Yanıtlar:


78

Bunu biraz açarak başlamalıyım ki neler olduğunu görebileyim:

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

Şimdi, çıkarma işleminin her bir tarafı için SQL Server'ın tam olarak hangi türleri kullandığını görelim:

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

Sonuçlar:

sayısal 5 2
sayısal 38 1

Yani 199.96bir numeric(5,2)daha uzun ve Floor(Cast(etc))bir numeric(38,1).

Oluşan hassasiyet ve ölçek için kurallar bir çıkarma işleminin (yani: e1 - e2) Böyle görünüm:

Hassasiyet: max (s1, s2) + max (p1-s1, p2-s2) + 1
Ölçek: max (s1, s2)

Bu şu şekilde değerlendirilir:

Hassasiyet: max (1,2) + max (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
Ölçek: max (1,2) => 2

numeric(38,1)İlk etapta nereden geldiğini anlamak için kurallar bağlantısını da kullanabilirsiniz (ipucu: iki duyarlık 19 değerini çarptınız).

Fakat:

  • Sonuç kesinliği ve ölçeğinin mutlak maksimum değeri 38'dir. Bir sonuç kesinliği 38'den büyük olduğunda, 38'e düşürülür ve bir sonucun ayrılmaz parçasının kesilmesini önlemek için karşılık gelen ölçek küçültülür. Çarpma veya bölme gibi bazı durumlarda, ondalık kesinliği korumak için ölçek faktörü azaltılmayacaktır, ancak taşma hatası yükseltilebilir.

Oops. Hassasiyet 40'tır. Bunu azaltmak zorundayız ve hassasiyeti azaltmak her zaman en önemsiz rakamları kesmesi gerektiğinden, bu da ölçeği azaltmak anlamına gelir. İfade için sonuçta ortaya çıkan son tür numeric(38,0), hangi 199.96yuvarlar için olacaktır 200.

Muhtemelen hareketli ve birleştirerek bu sorunu giderebilirsiniz CAST()büyük ifadenin içinden operasyonları biri CAST() tüm ifade sonucu etrafında. Yani bu:

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

Oluyor:

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

Dış kalıbı da çıkarabilirim.

Burada , beklenen sonuç yerine şu anda sahip olduğumuz hassasiyet ve ölçeğe uygun türleri seçmemiz gerektiğini öğreniyoruz . Sadece büyük kesinlikli sayılara gitmek mantıklı değil, çünkü SQL Server, taşmaları önlemek için aritmetik işlemler sırasında bu türleri değiştirecektir.


Daha fazla bilgi:


20

Aşağıdaki ifadeyle ilgili veri türlerine bir göz atın:

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)olup NUMERIC(38, 7)(aşağıya bakınız)
    • FLOOR(NUMERIC(38, 7))olup NUMERIC(38, 0)(aşağıya bakınız)
  2. 0.0 dır-dir NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0) dır-dir NUMERIC(38, 1)
  3. 199.96 dır-dir NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)olup NUMERIC(38, 1)(aşağıya bakınız)

İle sona açıklıyor 200.0( ondalık sonraki bir basamağa değil sıfır yerine) 199.96.

Notlar:

FLOORbelirtilen sayısal ifadeden küçük veya ona eşit en büyük tamsayıyı döndürür ve sonuç, input ile aynı türe sahiptir. INT için INT, FLOAT için FLOAT ve NUMERIC (x, y) için NUMERIC (x, 0) döndürür.

Algoritmaya göre :

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)

* Sonuç hassasiyeti ve ölçeğinin mutlak maksimum değeri 38'dir. Bir sonuç hassasiyeti 38'den büyük olduğunda, 38'e düşürülür ve bir sonucun ayrılmaz parçasının kesilmesini önlemek için karşılık gelen ölçek küçültülür.

Açıklama ayrıca toplama ve çarpma işlemlerinde ölçeğin tam olarak nasıl küçültüldüğünün ayrıntılarını da içerir. Bu açıklamaya göre:

  • NUMERIC(19, 4) * NUMERIC(19, 4)olduğu NUMERIC(39, 8)ve kenetlenmişNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)olduğu NUMERIC(40, 1)ve kenetlenmişNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)olduğu NUMERIC(40, 2)ve kenetlenmişNUMERIC(38, 1)

İşte algoritmayı JavaScript'te uygulama girişimim. Sonuçları SQL Server ile karşılaştırdım. Bu cevap özü sorunuzun kısmını.

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.