Чому 10 ^ 37/1 кидає арифметичну помилку переповнення?


11

Продовжуючи свою недавню тенденцію гри з великою кількістю , я нещодавно припустив помилку, з якою я стикався до наступного коду:

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;

Вихід, який я отримую для цього коду, є:

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

Що?

Чому перші 3 операції будуть працювати, але не останні? І як може виникнути арифметична помилка переповнення, якщо @big_numberявно можна зберегти вихід @big_number / 1?

Відповіді:


18

Розуміння точності та масштабу в контексті арифметичних операцій

Давайте розбимо це і детально розглянемо деталі арифметичного оператора ділення . Ось що має сказати MSDN про типи результатів оператора розділення :

Типи результатів

Повертає тип даних аргументу з більш високим пріоритетом. Для отримання додаткової інформації дивіться розділ Переважність типу даних (Transact-SQL) .

Якщо цілий дивіденд ділиться на цілий дільник, результат - це ціле число, яке має будь-яку дробову частину результату.

Ми знаємо, що @big_numberце DECIMAL. Який тип даних видає SQL Server 1? Це відкидає його INT. Ми можемо підтвердити це за допомогою 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
;

Для киків ми також можемо замінити 1в оригінальному блоці коду явно набране значення типу DECLARE @one INT = 1;і підтвердити, що ми отримуємо однакові результати.

Отже, у нас є DECIMALі INT. Оскільки DECIMALмає більший пріоритет типу даних, ніж INTми, ми знаємо, що вихід нашого підрозділу буде переданий DECIMAL.

То де проблема?

Проблема полягає у масштабі DECIMALвиходу. Ось таблиця правил про те, як SQL Server визначає точність та масштаб результатів, отриманих в результаті арифметичних операцій:

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.

І ось що ми маємо для змінних у цій таблиці:

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

Відповідно до коментаря зірочок до таблиці вище, максимальна точність DECIMALможе бути 38 . Таким чином, наша точність результатів знижується з 49 до 38, і "відповідна шкала зменшується, щоб запобігти обрізанню невід'ємної частини результату". З цього коментаря незрозуміло, як зменшується масштаб, але ми це знаємо:

Відповідно до формули в таблиці, мінімально можлива шкала, яку ви можете мати після ділення двох DECIMALs, дорівнює 6.

Таким чином, ми закінчуємо наступними результатами:

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.

Як це пояснює арифметичний перелив

Тепер відповідь очевидна:

Вихід нашого підрозділу може бути переданий DECIMAL(38, 6)та DECIMAL(38, 6)не може містити 10 37 .

З цим ми можемо побудувати ще один поділ, який досягає успіху, переконавшись, що результат може вміститися 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;

Результат:

10000000000000000000000000000000.000000

Зверніть увагу на 6 нулів після десяткових. Ми можемо підтвердити тип даних результату, DECIMAL(38, 6)використовуючи SQL_VARIANT_PROPERTY()наведене вище:

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
;

Небезпечне вирішення

То як нам обійти це обмеження?

Ну, це, безумовно, залежить від того, для чого ви робите ці розрахунки. Одне рішення, до якого ви можете негайно перейти, - це перетворити ваші номери FLOATдля обчислень, а потім перетворити їх назад, DECIMALколи ви закінчите.

Це може спрацювати за певних обставин, але вам слід бути обережними, щоб зрозуміти, що це за обставини. Як ми всі знаємо, перетворення чисел на та з FLOATних небезпечно і може дати неочікувані або неправильні результати.

У нашому випадку перетворення 10 37 в і з FLOATотримує результат, який є просто неправильним :

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
;

І ось у вас це є. Діліться уважно, мої діти.



2
RE: "Чистіший шлях". Ви можете подивитисяSQL_VARIANT_PROPERTY
Мартін Сміт

@Martin - Чи можете ви надати приклад або швидке пояснення того, як я можу використовувати SQL_VARIANT_PROPERTYдля виконання поділів, як той, що обговорювався у питанні?
Нік Чаммас

1
Тут є приклад (як альтернатива створенню нової таблиці для визначення типу даних)
Мартін Сміт

@Martin - Ага так, це набагато акуратніше!
Нік Чаммас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.