Mengapa 10 ^ 37/1 melempar kesalahan aritmatika overflow?

11

Melanjutkan tren saya baru-baru ini bermain dengan jumlah besar , saya baru-baru ini merebus kesalahan saya berlari ke kode berikut:

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;

Output yang saya dapatkan untuk kode ini adalah:

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

Apa?

Mengapa 3 operasi pertama bekerja tetapi bukan yang terakhir? Dan bagaimana bisa ada kesalahan aritmatika overflow jika @big_numberjelas dapat menyimpan output @big_number / 1?

Nick Chammas
sumber

Jawaban:

18

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_numberadalah 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 1blok kode asli dengan nilai yang diketik secara eksplisit seperti DECLARE @one INT = 1;dan mengonfirmasi bahwa kita mendapatkan hasil yang sama.

Jadi kita punya DECIMALdan INT. Karena DECIMALmemiliki tipe data yang lebih diutamakan daripada INT, kita tahu output dari divisi kami akan dilemparkan ke DECIMAL.

Jadi dimana masalahnya?

Masalahnya adalah dengan skala DECIMALdalam 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 DECIMALbisa 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 DECIMALadalah 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 FLOATuntuk perhitungan, dan kemudian mengonversinya kembali DECIMALketika 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 FLOATberbahaya dan dapat memberi Anda hasil yang tidak terduga atau salah.

Dalam kasus kami, mengonversi 10 37 ke dan dari FLOATmendapatkan 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.

Nick Chammas
sumber
2
RE: "Cleaner Way". Anda mungkin ingin melihatSQL_VARIANT_PROPERTY
Martin Smith
@ Martin - Bisakah Anda memberikan contoh atau penjelasan singkat tentang bagaimana saya dapat menggunakan SQL_VARIANT_PROPERTYuntuk melakukan divisi seperti yang dibahas dalam pertanyaan?
Nick Chammas
1
Ada contoh di sini (sebagai alternatif untuk membuat tabel baru untuk menentukan tipe data)
Martin Smith
@ Martin - Ah ya, itu jauh lebih rapi!
Nick Chammas