เหตุใด 199.96 - 0 = 200 ใน SQL


84

ฉันมีลูกค้าบางคนได้รับตั๋วเงินแปลก ๆ ฉันสามารถแยกปัญหาหลัก:

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

มีใครรู้เบาะแสอะไรเกิดขึ้นที่นี่? ฉันหมายความว่ามันมีบางอย่างเกี่ยวข้องกับประเภทข้อมูลทศนิยม แต่ฉันไม่สามารถคาดเดาได้จริงๆ ...


มีความสับสนมากเกี่ยวกับประเภทข้อมูลที่เป็นจำนวนตามตัวอักษรดังนั้นฉันจึงตัดสินใจที่จะแสดงบรรทัดจริง:

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)ถูกเหวี่ยงอย่างชัดเจนก่อนที่จะนำไปใช้กับบริบทภายนอก

200.00อย่างไรก็ตามผลซาก


ตอนนี้ฉันได้สร้างตัวอย่างที่ต้มแล้วพวกคุณสามารถดำเนินการบนคอมพิวเตอร์ของคุณได้

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

ตอนนี้ฉันมีบางอย่าง ...

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

สิ่งที่ชั้นควรจะคืนจำนวนเต็มอยู่แล้ว เกิดอะไรขึ้นที่นี่? :-D


ฉันคิดว่าตอนนี้ฉันสามารถต้มมันลงไปถึงสาระสำคัญได้แล้ว :-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 การร่ายเหล่านั้นทั้งหมดและพื้นที่มีการแปลงโดยนัยเป็นจุดลอยตัวและด้านหลังจะรับประกันว่าจะทำให้สูญเสียความแม่นยำ
Panagiotis Kanavos

1
@Silverdust ถ้ามันมาจากโต๊ะเท่านั้น ตามตัวอักษรในนิพจน์อาจเป็นfloat
Panagiotis Kanavos

1
โอ้ ... และFloor()ไม่ได้intกลับ จะส่งคืนประเภทเดียวกันกับนิพจน์ดั้งเดิมโดยลบส่วนทศนิยมออก สำหรับส่วนที่เหลือIIF()ฟังก์ชันจะส่งผลให้ประเภทที่มีลำดับความสำคัญสูงสุด ( docs.microsoft.com/en-us/sql/t-sql/functions/… ) ดังนั้นตัวอย่างที่ 2 ที่คุณร่ายเป็น int ความสำคัญที่สูงกว่าคือการร่ายแบบธรรมดาเป็นตัวเลข (19,4)
Joel Coehoorn

1
คำตอบที่ดี (ใครจะรู้ว่าคุณสามารถตรวจสอบข้อมูลเมตาของตัวแปร sql ได้) แต่ในปี 2012 ฉันได้ผลลัพธ์ที่คาดหวัง (199.96)
benjamin moskovits

2
ฉันไม่ค่อยคุ้นเคยกับ MS SQL แต่ฉันต้องบอกว่าการดูการดำเนินการแคสต์ทั้งหมดและอื่น ๆ ทำให้ฉันได้รับความสนใจอย่างรวดเร็ว .. ดังนั้นฉันต้องเชื่อมโยงสิ่งนี้เพราะไม่ควรมีใครใช้floatประเภทจุดเข้าในการจัดการสกุลเงิน .
code_dredd

คำตอบ:


78

ฉันต้องเริ่มต้นด้วยการแกะมันออกเล็กน้อยเพื่อที่จะได้เห็นว่าเกิดอะไรขึ้น:

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

ตอนนี้เรามาดูกันว่า SQL Server ใช้ประเภทใดสำหรับการลบแต่ละด้าน:

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

ผล:

ตัวเลข 5 2
ตัวเลข 38 1

ดังนั้น199.96เป็นnumeric(5,2)และอีกคือFloor(Cast(etc))numeric(38,1)

กฎสำหรับความแม่นยำที่เกิดและขนาดของการดำเนินการลบ (เช่น: e1 - e2) ลักษณะเช่นนี้

ความแม่นยำ:สูงสุด (s1, s2) + สูงสุด (p1-s1, p2-s2) + 1
มาตราส่วน: สูงสุด (s1, s2)

ที่ประเมินดังนี้:

ความแม่นยำ:สูงสุด (1,2) + สูงสุด (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
มาตราส่วน:สูงสุด (1,2) => 2

คุณยังสามารถใช้ลิงก์กฎเพื่อดูว่าnumeric(38,1)มาจากที่ใดในตอนแรก (คำใบ้: คุณคูณค่าความแม่นยำ 19 ค่าสองค่า)

แต่:

  • ความแม่นยำของผลลัพธ์และมาตราส่วนมีค่าสูงสุดสัมบูรณ์ที่ 38 เมื่อความแม่นยำของผลลัพธ์มากกว่า 38 ความแม่นยำของผลลัพธ์จะลดลงเหลือ 38 และมาตราส่วนที่เกี่ยวข้องจะลดลงเพื่อพยายามป้องกันไม่ให้ส่วนหนึ่งของผลลัพธ์ถูกตัดทอน ในบางกรณีเช่นการคูณหรือการหารตัวคูณมาตราส่วนจะไม่ลดลงเพื่อรักษาความแม่นยำของทศนิยมแม้ว่าจะมีข้อผิดพลาดมากเกินไป

อ๊ะ. ความแม่นยำคือ 40 เราต้องลดและเนื่องจากการลดความแม่นยำควรตัดตัวเลขที่มีนัยสำคัญน้อยที่สุดออกไปซึ่งหมายถึงการลดขนาดด้วย ชนิดที่ส่งผลขั้นสุดท้ายสำหรับการแสดงออกจะเป็นnumeric(38,0)ซึ่งสำหรับรอบ199.96200

คุณสามารถแก้ไขปัญหานี้ได้โดยการย้ายและรวมการCAST()ดำเนินการจากภายในนิพจน์ขนาดใหญ่เป็นหนึ่ง CAST()รอบผลลัพธ์นิพจน์ทั้งหมด ดังนั้นสิ่งนี้:

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

กลายเป็น:

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

ฉันอาจจะถอดเฝือกด้านนอกออกด้วย

เราเรียนรู้ที่นี่เราควรเลือกประเภทให้ตรงกับความแม่นยำและมาตราส่วนที่เรามีในตอนนี้แทนที่จะเป็นผลลัพธ์ที่คาดหวัง มันไม่สมเหตุสมผลที่จะใช้ตัวเลขที่มีความแม่นยำสูงเนื่องจาก SQL Server จะกลายพันธุ์ประเภทเหล่านั้นในระหว่างการดำเนินการทางคณิตศาสตร์เพื่อพยายามหลีกเลี่ยงการล้น


ข้อมูลมากกว่านี้:


20

จับตาดูประเภทข้อมูลที่เกี่ยวข้องสำหรับข้อความต่อไปนี้:

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)(ดูด้านล่าง)
    • FLOOR(NUMERIC(38, 7))คือNUMERIC(38, 0)(ดูด้านล่าง)
  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)(ดูด้านล่าง)

นี้อธิบายว่าทำไมคุณท้ายด้วย200.0( หนึ่งหลักหลังจากทศนิยมไม่เป็นศูนย์ ) 199.96แทน

หมายเหตุ:

FLOORส่งคืนจำนวนเต็มที่มากที่สุดน้อยกว่าหรือเท่ากับนิพจน์ตัวเลขที่ระบุและผลลัพธ์มีประเภทเดียวกับอินพุต ส่งกลับ INT สำหรับ INT, FLOAT สำหรับ FLOAT และ NUMERIC (x, 0) สำหรับ NUMERIC (x, y)

ตามอัลกอริทึม :

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)

* ความแม่นยำของผลลัพธ์และมาตราส่วนมีค่าสูงสุดสัมบูรณ์ที่ 38 เมื่อความแม่นยำของผลลัพธ์มากกว่า 38 ความแม่นยำของผลลัพธ์จะลดลงเหลือ 38 และมาตราส่วนที่เกี่ยวข้องจะลดลงเพื่อพยายามป้องกันไม่ให้ส่วนหนึ่งของผลลัพธ์ถูกตัดทอน

คำอธิบายยังมีรายละเอียดว่าสเกลนั้นลดลงอย่างไรภายในการดำเนินการบวกและการคูณ ตามคำอธิบายนั้น:

  • NUMERIC(19, 4) * NUMERIC(19, 4)ถูกNUMERIC(39, 8)ยึดเข้ากับNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)ถูกNUMERIC(40, 1)ยึดเข้ากับNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)ถูกNUMERIC(40, 2)ยึดเข้ากับNUMERIC(38, 1)

นี่คือความพยายามของฉันที่จะใช้อัลกอริทึมใน JavaScript ฉันได้ตรวจสอบผลลัพธ์กับ SQL Server แล้ว มันตอบส่วนสำคัญของคำถามของคุณ

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.