เหตุใด 10 ^ 37/1 จึงเกิดข้อผิดพลาดทางคณิตศาสตร์มากเกินไป


11

ต่อแนวโน้มล่าสุดของฉันในการเล่นกับจำนวนมากฉันเพิ่งต้มข้อผิดพลาดที่ฉันกำลังทำงานลงไปที่รหัสต่อไปนี้:

DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);

PRINT @big_number + 1;
PRINT @big_number - 1;
PRINT @big_number * 1;
PRINT @big_number / 1;

ผลลัพธ์ที่ฉันได้รับสำหรับรหัสนี้คือ:

10000000000000000000000000000000000001
9999999999999999999999999999999999999
10000000000000000000000000000000000000
Msg 8115, Level 16, State 2, Line 6
Arithmetic overflow error converting expression to data type numeric.

อะไร?

ทำไมการดำเนินการ 3 รายการแรกถึงทำงานได้ แต่ไม่ใช่ครั้งสุดท้าย และวิธีการที่อาจมีข้อผิดพลาดล้นเลขคณิตถ้า@big_numberเห็นได้ชัดว่าสามารถจัดเก็บการส่งออกของ@big_number / 1?

คำตอบ:


18

การทำความเข้าใจความแม่นยำและขนาดในบริบทของการดำเนินการทางคณิตศาสตร์

Let 's ทำลายลงนี้และใช้เวลามองใกล้ที่รายละเอียดของการแบ่งผู้ประกอบการทางคณิตศาสตร์ นี่คือสิ่งที่ MSDN พูดถึงเกี่ยวกับประเภทผลลัพธ์ของตัวดำเนินการหาร :

ประเภทผลลัพธ์

ส่งคืนชนิดข้อมูลของอาร์กิวเมนต์ที่มีลำดับความสำคัญสูงกว่า สำหรับข้อมูลเพิ่มเติมโปรดดูที่ชนิดของข้อมูลเป็นผู้นำ (Transact SQL)

หากการหารจำนวนเต็มหารด้วยตัวหารจำนวนเต็มผลลัพธ์จะเป็นจำนวนเต็มที่มีส่วนที่เป็นเศษส่วนของผลลัพธ์ที่ถูกตัดทอน

เรารู้ว่าเป็น@big_number DECIMALสิ่งที่ชนิดข้อมูล SQL Server ไม่โยน1เป็น? INTมันได้ปลดเปลื้องไปยัง เราสามารถยืนยันสิ่งนี้ได้ด้วยความช่วยเหลือของSQL_VARIANT_PROPERTY():

SELECT
      SQL_VARIANT_PROPERTY(1, 'BaseType')   AS [BaseType]  -- int
    , SQL_VARIANT_PROPERTY(1, 'Precision')  AS [Precision] -- 10
    , SQL_VARIANT_PROPERTY(1, 'Scale')      AS [Scale]     -- 0
;

สำหรับการเตะเรายังสามารถแทนที่1ในบล็อกรหัสต้นฉบับด้วยค่าที่พิมพ์อย่างชัดเจนเช่นDECLARE @one INT = 1;และยืนยันว่าเราได้ผลลัพธ์เดียวกัน

ดังนั้นเราจึงมีและDECIMAL INTเนื่องจากDECIMALมีความสูงชนิดข้อมูลมีความสำคัญกว่าเรารู้ว่าการส่งออกของส่วนของเราจะถูกส่งไปยัง INTDECIMAL

ดังนั้นปัญหาอยู่ที่ไหน

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

Operation                              Result precision                       Result scale *
-------------------------------------------------------------------------------------------------
e1 + e2                                max(s1, s2) + max(p1-s1, p2-s2) + 1    max(s1, s2)
e1 - e2                                max(s1, s2) + max(p1-s1, p2-s2) + 1    max(s1, s2)
e1 * e2                                p1 + p2 + 1                            s1 + s2
e1 / e2                                p1 - s1 + s2 + max(6, s1 + p2 + 1)     max(6, s1 + p2 + 1)
e1 { UNION | EXCEPT | INTERSECT } e2   max(s1, s2) + max(p1-s1, p2-s2)        max(s1, s2)
e1 % e2                                min(p1-s1, p2 -s2) + max( s1,s2 )      max(s1, s2)

* The result precision and scale have an absolute maximum of 38. When a result 
  precision is greater than 38, the corresponding scale is reduced to prevent the 
  integral part of a result from being truncated.

และนี่คือสิ่งที่เรามีสำหรับตัวแปรในตารางนี้:

e1: @big_number, a DECIMAL(38, 0)
-> p1: 38
-> s1: 0

e2: 1, an INT
-> p2: 10
-> s2: 0

e1 / e2
-> Result precision: p1 - s1 + s2 + max(6, s1 + p2 + 1) = 38 + max(6, 11) = 49
-> Result scale:                    max(6, s1 + p2 + 1) =      max(6, 11) = 11

ต่อความคิดเห็นดอกจันบนโต๊ะข้างต้นที่มีความแม่นยำสูงสุดDECIMALสามารถมี 38 ดังนั้นความแม่นยำของผลลัพธ์ของเราจึงลดลงจาก 49 เป็น 38 และ "ลดขนาดที่สอดคล้องกันเพื่อป้องกันไม่ให้ส่วนที่สำคัญของผลลัพธ์ถูกตัดทอน" ยังไม่ชัดเจนจากความคิดเห็นนี้ว่าระดับลดลงได้อย่างไร แต่เรารู้สิ่งนี้:

ตามสูตรการคำนวณในตารางที่ขั้นต่ำขนาดที่เป็นไปได้คุณสามารถมีหลังจากหารสองDECIMALคือ 6

ดังนั้นเราจะได้ผลลัพธ์ต่อไปนี้:

e1 / e2
-> Result precision: 49 -> reduced to 38
-> Result scale:     11 -> reduced to 6  

Note that 6 is the minimum possible scale it can be reduced to. 
It may be between 6 and 11 inclusive.

วิธีนี้อธิบายถึงการล้นทางคณิตศาสตร์

ตอนนี้คำตอบนั้นชัดเจน:

ผลลัพธ์ของแผนกของเราถูกส่งไปDECIMAL(38, 6)และDECIMAL(38, 6)ไม่สามารถถือ 10 37ได้

ด้วยสิ่งนี้เราสามารถสร้างแผนกอื่นที่ประสบความสำเร็จได้ด้วยการทำให้แน่ใจว่าผลลัพธ์สามารถเข้าDECIMAL(38, 6)กันได้:

DECLARE @big_number    DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million   INT           = '1' + REPLICATE(0, 6);

PRINT @big_number / @one_million;

ผลลัพธ์คือ:

10000000000000000000000000000000.000000

สังเกต 6 ศูนย์หลังจุดทศนิยม เราสามารถยืนยันประเภทข้อมูลของผลลัพธ์ได้DECIMAL(38, 6)โดยใช้SQL_VARIANT_PROPERTY()ด้านบน:

DECLARE @big_number   DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million  INT           = '1' + REPLICATE(0, 6);

SELECT
      SQL_VARIANT_PROPERTY(@big_number / @one_million, 'BaseType')  AS [BaseType]  -- decimal
    , SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Precision') AS [Precision] -- 38
    , SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Scale')     AS [Scale]     -- 6
;

วิธีแก้ปัญหาอันตราย

ดังนั้นเราจะแก้ไขข้อ จำกัด นี้ได้อย่างไร

แน่นอนว่าขึ้นอยู่กับสิ่งที่คุณทำการคำนวณเหล่านี้ ทางออกหนึ่งที่คุณสามารถข้ามไปได้ทันทีคือการแปลงตัวเลขของคุณเป็นFLOATสำหรับการคำนวณแล้วแปลงกลับเป็นDECIMALเมื่อคุณทำเสร็จแล้ว

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

ในกรณีของเราการแปลง 10 37ไปเป็นจากนั้นFLOATจะได้ผลลัพธ์ที่ผิดธรรมดา:

DECLARE @big_number     DECIMAL(38,0)  = '1' + REPLICATE(0, 37);
DECLARE @big_number_f   FLOAT          = CAST(@big_number AS FLOAT);

SELECT
      @big_number                           AS big_number      -- 10^37
    , @big_number_f                         AS big_number_f    -- 10^37
    , CAST(@big_number_f AS DECIMAL(38, 0)) AS big_number_f_d  -- 9999999999999999.5 * 10^21
;

และคุณมีมัน แบ่งอย่างระมัดระวังลูก ๆ ของฉัน



2
RE: "วิธีทำความสะอาด" คุณอาจต้องการดูSQL_VARIANT_PROPERTY
Martin Smith

@Martin - คุณช่วยยกตัวอย่างหรือคำอธิบายสั้น ๆ เกี่ยวกับวิธีที่ฉันสามารถใช้SQL_VARIANT_PROPERTYเพื่อดำเนินการดิวิชั่นเหมือนที่กล่าวไว้ในคำถามได้หรือไม่?
Nick Chammas

1
มีตัวอย่างอยู่ที่นี่ (เป็นอีกทางเลือกหนึ่งในการสร้างตารางใหม่เพื่อกำหนดประเภทข้อมูล)
Martin Smith

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