Memahami Presisi dan Skala dalam konteks Operasi Aritmatika
Mari kita uraikan ini dan perhatikan detail dari operator aritmatika pembagian . Inilah yang dikatakan MSDN tentang tipe hasil dari operator bagi :
Jenis hasil
Mengembalikan tipe data dari argumen dengan prioritas yang lebih tinggi. Untuk informasi lebih lanjut, lihat Data Type Precedence (Transact-SQL) .
Jika dividen integer dibagi oleh pembagi integer, hasilnya adalah integer yang memiliki bagian fraksional dari hasil terpotong.
Kita tahu itu @big_number
adalah DECIMAL
. Tipe data apa yang digunakan SQL Server 1
? Itu melemparkannya ke INT
. Kami dapat mengkonfirmasi ini dengan bantuan 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
;
Untuk tendangan, kita juga bisa mengganti 1
blok kode asli dengan nilai yang diketik secara eksplisit seperti DECLARE @one INT = 1;
dan mengonfirmasi bahwa kita mendapatkan hasil yang sama.
Jadi kita punya DECIMAL
dan INT
. Karena DECIMAL
memiliki tipe data yang lebih diutamakan daripada INT
, kita tahu output dari divisi kami akan dilemparkan ke DECIMAL
.
Jadi dimana masalahnya?
Masalahnya adalah dengan skala DECIMAL
dalam output. Berikut adalah tabel aturan tentang bagaimana SQL Server menentukan ketepatan dan skala hasil yang diperoleh dari operasi aritmatika:
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.
Dan inilah yang kita miliki untuk variabel-variabel dalam tabel ini:
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
Per komentar asterisk pada tabel di atas, presisi maksimum yang DECIMAL
bisa dimiliki adalah 38 . Jadi ketepatan hasil kami dikurangi dari 49 menjadi 38, dan "skala terkait dikurangi untuk mencegah bagian integral dari hasil terpotong." Tidak jelas dari komentar ini bagaimana skala berkurang, tetapi kami tahu ini:
Menurut rumus dalam tabel, skala minimum yang mungkin Anda miliki setelah membagi dua DECIMAL
adalah 6.
Jadi, kita berakhir dengan hasil berikut:
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.
Bagaimana ini Menjelaskan Overflow Aritmatika
Sekarang jawabannya sudah jelas:
Output dari divisi kami dilemparkan ke DECIMAL(38, 6)
, dan DECIMAL(38, 6)
tidak dapat menampung 10 37 .
Dengan itu, kita dapat membangun divisi lain yang berhasil dengan memastikan hasilnya dapat masuk 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;
Hasilnya adalah:
10000000000000000000000000000000.000000
Catat 6 nol setelah desimal. Kami dapat mengonfirmasi tipe data hasil adalah DECIMAL(38, 6)
dengan menggunakan SQL_VARIANT_PROPERTY()
seperti di atas:
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
;
Solusi yang Berbahaya
Jadi bagaimana kita mengatasi batasan ini?
Yah, itu tentu tergantung pada apa Anda membuat perhitungan ini. Salah satu solusi yang dapat langsung Anda gunakan adalah mengonversi angka menjadi FLOAT
untuk perhitungan, dan kemudian mengonversinya kembali DECIMAL
ketika Anda selesai.
Itu mungkin berhasil dalam beberapa keadaan, tetapi Anda harus berhati-hati untuk memahami apa keadaan itu. Seperti yang kita semua tahu, mengubah angka ke dan dari FLOAT
berbahaya dan dapat memberi Anda hasil yang tidak terduga atau salah.
Dalam kasus kami, mengonversi 10 37 ke dan dari FLOAT
mendapatkan hasil yang benar-benar salah :
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
;
Dan begitulah. Bagilah dengan hati-hati, anak-anakku.