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_number
là 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ế 1
khố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 DECIMAL
và một INT
. Vì DECIMAL
có quyề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 DECIMAL
có 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 DECIMAL
s 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)
và 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 để FLOAT
tính toán, sau đó chuyển đổi chúng trở lại DECIMAL
khi 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ừ FLOAT
mộ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.