10 ^ 37/1 neden aritmetik taşma hatası veriyor?


11

Son zamanlarda büyük sayılarla oynama eğilimimi sürdürürken , son zamanlarda aşağıdaki koda koştuğum bir hatayı kaynattım:

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;

Bu kod için aldığım çıktı:

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

Ne?

İlk 3 operasyon neden işe yaradı ama sonuncusu olmasın? Ve @big_numberaçık bir şekilde çıkışını saklayabilirse aritmetik bir taşma hatası nasıl olabilir @big_number / 1?

Yanıtlar:


18

Hassasiyet ve Ölçeği Aritmetik İşlemler İçinde Anlamak

Bunu parçalayalım ve bölme aritmetik operatörünün ayrıntılarına yakından bakalım . MSDN'nin bölme işlecinin sonuç türleri hakkında söyledikleri :

Sonuç Türleri

Daha yüksek önceliğe sahip bağımsız değişkenin veri türünü döndürür. Daha fazla bilgi için, bkz. Veri Türü Önceliği (Transact-SQL) .

Bir tamsayı temettüsü bir tamsayı bölenle bölünürse, sonuç, sonucun kesirli kısmının kesilmiş olduğu bir tamsayıdır.

Bunu biliyoruz @big_numberbir olduğunu DECIMAL. SQL Server hangi veri türünü kullanıyor 1? Bir INT. Bunu aşağıdakilerin yardımıyla onaylayabiliriz 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
;

Tekmeler için, 1orijinal kod bloğundaki "gibi" açıkça yazılan bir değerle değiştirebilir DECLARE @one INT = 1;ve aynı sonuçları aldığımızı onaylayabiliriz.

Yani bir DECIMALve bir INT. Daha DECIMALyüksek bir veri türü önceliğine sahip olduğundan INT, bölümümüzün çıktısının verileceğini biliyoruz DECIMAL.

Peki sorun nerede?

Sorun, DECIMALçıktıdaki ölçeği ile ilgilidir . SQL Server'ın aritmetik işlemlerden elde edilen sonuçların kesinliğini ve ölçeğini nasıl belirlediğine ilişkin kurallar tablosu :

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.

Ve bu tablodaki değişkenler için elimizde ne var:

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

Yukarıdaki tablodaki yıldız işaretine göre, a'nın sahip olabileceği maksimum hassasiyet DECIMAL38'dir . Böylece sonuç hassasiyetimiz 49'dan 38'e düşürülür ve "bir sonucun ayrılmaz kısmının kesilmesini önlemek için karşılık gelen ölçek azaltılır." Bu yorumdan ölçeğin nasıl azaltıldığı açık değildir , ancak bunu biliyoruz:

Tablodaki formüle göre, iki s'yi böldükten sonra sahip olabileceğiniz minimum ölçek DECIMAL6'dır.

Böylece, aşağıdaki sonuçlarla sonuçlanır:

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.

Bu, Aritmetik Taşmayı Nasıl Açıklar?

Şimdi cevap açık:

Bölümümüzün çıktısı alınıyor DECIMAL(38, 6)ve DECIMAL(38, 6)10 37'yi tutamıyor .

Bunun üzerine, biz emin sonucu yaparak başarılı başka bölme oluşturabilirsiniz yapabilirsiniz sığacak 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;

Sonuç:

10000000000000000000000000000000.000000

Ondalıktan sonraki 6 sıfırı not edin. Biz sonucun veri türüdür teyit edebilir DECIMAL(38, 6)kullanarak SQL_VARIANT_PROPERTY()yukarıdaki gibi:

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
;

Tehlikeli Bir Çözüm

Peki bu sınırlamayı nasıl aşabiliriz?

Bu kesinlikle bu hesaplamaları ne yaptığınıza bağlıdır. Hemen atlayabileceğiniz bir çözüm FLOAT, hesaplamalar için sayılarınızı dönüştürmek ve DECIMALişiniz bittiğinde bunları geri dönüştürmektir .

Bu bazı durumlarda işe yarayabilir, ancak bu koşulların ne olduğunu anlamaya dikkat etmelisiniz. Hepimizin bildiği gibi, sayıları buraya ve aralarından dönüştürmek FLOATtehlikelidir ve size beklenmedik veya yanlış sonuçlar verebilir.

Bizim durumumuzda, 10 dönüştürme 37 ve gelen FLOATsadece düz bir sonuç alır yanlış :

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
;

İşte buyur. Dikkatlice bölün, çocuklarım.



2
RE: "Temiz Yol". Sen bakmak isteyebilirsinizSQL_VARIANT_PROPERTY
Martin Smith

@Martin - Soruda SQL_VARIANT_PROPERTYtartışılan gibi bölümleri gerçekleştirmek için nasıl kullanabileceğime örnek veya hızlı bir açıklama verebilir misiniz?
Nick Chammas

1
Burada bir örnek var (veri türünü belirlemek için yeni bir tablo oluşturmaya alternatif olarak)
Martin Smith

@Martin - Ah evet, bu çok daha temiz!
Nick Chammas
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.