Tại sao 10 ^ 37/1 lại xảy ra lỗi tràn số học?


11

Tiếp tục xu hướng chơi gần đây của tôi với số lượng lớn , gần đây tôi đã gặp phải một lỗi tôi đang chạy theo mã sau:

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;

Đầu ra tôi nhận được cho mã này là:

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

Gì?

Tại sao 3 hoạt động đầu tiên sẽ làm việc nhưng không phải là hoạt động cuối cùng? Và làm thế nào có thể có một lỗi tràn số học nếu @big_numberrõ ràng có thể lưu trữ đầu ra của @big_number / 1?

Câu trả lời:


18

Hiểu chính xác và quy mô trong bối cảnh hoạt động số học

Chúng ta hãy phá vỡ điều này và xem xét chi tiết về toán tử số học chia . Đây là những gì MSDN nói về các loại kết quả của toán tử chia :

Các loại kết quả

Trả về kiểu dữ liệu của đối số có độ ưu tiên cao hơn. Để biết thêm thông tin, hãy xem Ưu tiên loại dữ liệu (Transact-SQL) .

Nếu một cổ tức số nguyên được chia cho một ước số nguyên, kết quả là một số nguyên có bất kỳ phần phân số nào của kết quả bị cắt cụt.

Chúng tôi biết đó @big_numberlà một DECIMAL. SQL Server sử dụng kiểu dữ liệu nào 1? Nó đưa nó đến một INT. Chúng tôi có thể xác nhận điều này với sự giúp đỡ của 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
;

Đối với các cú đá, chúng ta cũng có thể thay thế 1khối mã gốc bằng một giá trị được gõ rõ ràng như DECLARE @one INT = 1;và xác nhận rằng chúng ta nhận được kết quả tương tự.

Vì vậy, chúng tôi có một DECIMALvà một INT. Vì DECIMALquyền ưu tiên loại dữ liệu cao hơn INT, chúng tôi biết đầu ra của bộ phận của chúng tôi sẽ được chuyển sang DECIMAL.

Vậy vấn đề ở đâu?

Vấn đề là với quy mô của DECIMALđầu ra. Dưới đây là bảng quy tắc về cách SQL Server xác định độ chính xác và tỷ lệ kết quả thu được từ các phép toán số học:

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.

Và đây là những gì chúng ta có cho các biến trong bảng này:

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

Theo nhận xét hoa thị trên bảng trên, độ chính xác tối đa DECIMALcó thể có là 38 . Vì vậy, độ chính xác kết quả của chúng tôi bị cắt giảm từ 49 xuống 38 và "thang đo tương ứng được giảm để ngăn phần không thể thiếu của kết quả bị cắt ngắn". Không rõ từ nhận xét này làm thế nào để giảm quy mô, nhưng chúng tôi biết điều này:

Theo công thức trong bảng, thang đo tối thiểu bạn có thể có sau khi chia hai DECIMALs là 6.

Vì vậy, chúng tôi kết thúc với kết quả sau đây:

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.

Làm thế nào điều này giải thích tràn số học

Bây giờ câu trả lời là rõ ràng:

Đầu ra của bộ phận của chúng tôi được chuyển sang DECIMAL(38, 6)DECIMAL(38, 6)không thể giữ 10 37 .

Cùng với đó, chúng ta có thể xây dựng một bộ phận khác thành công bằng cách đảm bảo kết quả có thể phù hợp với 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;

Kết quả là:

10000000000000000000000000000000.000000

Lưu ý 6 số không sau số thập phân. Chúng tôi có thể xác nhận loại dữ liệu của kết quả DECIMAL(38, 6)bằng cách sử dụng SQL_VARIANT_PROPERTY()như trên:

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
;

Cách giải quyết nguy hiểm

Vậy làm thế nào để chúng ta vượt qua giới hạn này?

Chà, điều đó chắc chắn phụ thuộc vào những gì bạn đang thực hiện những tính toán này. Một giải pháp bạn có thể ngay lập tức chuyển sang là chuyển đổi số của mình để FLOATtính toán, sau đó chuyển đổi chúng trở lại DECIMALkhi bạn hoàn thành.

Điều đó có thể làm việc trong một số trường hợp, nhưng bạn nên cẩn thận để hiểu những trường hợp đó là gì. Như chúng ta đã biết, chuyển đổi số sang và từFLOAT là nguy hiểm và có thể mang lại cho bạn kết quả bất ngờ hoặc không chính xác.

Trong trường hợp của chúng tôi, chuyển đổi 10 37 sang và từ FLOATmột kết quả hoàn toàn sai :

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
;

Và bạn có nó rồi đấy! Chia cẩn thận nhé các con.



2
RE: "Cách sạch hơn". Bạn có thể muốn xem xétSQL_VARIANT_PROPERTY
Martin Smith

@Martin - Bạn có thể cung cấp một ví dụ hoặc giải thích nhanh về cách tôi có thể sử dụng SQL_VARIANT_PROPERTYđể thực hiện các phân chia như phân chia trong câu hỏi không?
Nick Chammas

1
Có một ví dụ ở đây (thay thế cho việc tạo bảng mới để xác định kiểu dữ liệu)
Martin Smith

@Martin - À đúng rồi, gọn gàng hơn nhiều!
Nick Chammas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.