Чому 199.96 - 0 = 200 в SQL?


84

У мене деякі клієнти отримують дивні рахунки. Мені вдалося виділити основну проблему:

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 200 what the?
SELECT 199.96 - (0.0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96)) -- 199.96

SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 199.96

-- It gets weirder...
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 0
SELECT (0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 0
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 0

-- so... ... 199.06 - 0 equals 200... ... right???
SELECT 199.96 - 0 -- 199.96 ...NO....

Хто-небудь підказує, що, біса, тут відбувається? Я маю на увазі, це, безумовно, пов’язано з десятковим типом даних, але я не можу насправді обернути його головою ...


Було багато плутанини щодо того, яким типом даних є числові літерали, тому я вирішив показати справжній рядок:

PS.SharePrice - (CAST((@InstallmentCount - 1) AS DECIMAL(19, 4)) * CAST(FLOOR(@InstallmentPercent * PS.SharePrice) AS DECIMAL(19, 4))))

PS.SharePrice DECIMAL(19, 4)

@InstallmentCount INT

@InstallmentPercent DECIMAL(19, 4)

Я переконався, що результат кожної операції, що має операнд типу, відмінного від того DECIMAL(19, 4), що відтворюється явно, перед застосуванням його до зовнішнього контексту.

Тим не менше, результат залишається 200.00.


Зараз я створив зведений зразок, який ви можете виконати на своєму комп’ютері.

DECLARE @InstallmentIndex INT = 1
DECLARE @InstallmentCount INT = 1
DECLARE @InstallmentPercent DECIMAL(19, 4) = 1.0
DECLARE @PS TABLE (SharePrice DECIMAL(19, 4))
INSERT INTO @PS (SharePrice) VALUES (599.96)

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * PS.SharePrice),
  1999.96)
FROM @PS PS

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(599.96 AS DECIMAL(19, 4))),
  1999.96)
FROM @PS PS

-- 1996.96
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * 599.96),
  1999.96)
FROM @PS PS

-- Funny enough - with this sample explicitly converting EVERYTHING to DECIMAL(19, 4) - it still doesn't work...
-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))
FROM @PS PS

Тепер у мене щось є ...

-- 2000
SELECT
  IIF(1 = 2,
  FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))

-- 1999.9600
SELECT
  IIF(1 = 2,
  CAST(FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) AS INT),
  CAST(1999.96 AS DECIMAL(19, 4)))

Якого біса - поверх все одно повинен повернути ціле число. Що тут відбувається? :-D


Думаю, мені зараз вдалося по-справжньому звести це до самої суті :-D

-- 1.96
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (36, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2.0
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (37, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (38, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

4
@Sliverdust 199.96 -0 не дорівнює 200. Усі ці відливки та підлоги з неявними перетвореннями в плаваючу крапку і назад, хоча гарантовано призведуть до втрати точності.
Панайотис Канавос

1
@Silverdust, лише якщо він надходив зі столу. Буквально у виразі це, мабуть,float
Panagiotis Kanavos

1
Про ... і Floor()зовсім НЕ повертати int. Він повертає той самий тип, що і вихідний вираз , із видаленою десятковою частиною. В іншому випадку IIF()функція отримує тип із найвищим пріоритетом ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Отже, другий зразок, де ви перекладаєте на int, вищим пріоритетом є простий привід як числовий (19,4).
Joel Coehoorn,

1
Чудова відповідь (хто знав, що ви можете вивчити метадані варіанту sql?), Але в 2012 році я отримую очікувані результати (199,96).
Бенджамін Московіц,

2
Я не надто знайомий з MS SQL, але мушу сказати, що перегляд усіх цих операцій приведення і так далі швидко привернув мою увагу .. тому я повинен зв’язати це, тому що ніхто ніколи не повинен використовувати floatтипи ing-point для обробки валюти .
code_dredd

Відповіді:


78

Мені потрібно почати з того, щоб трохи розгорнути це, щоб я міг побачити, що відбувається:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

Тепер давайте подивимося, які саме типи використовує SQL Server для кожної сторони операції віднімання:

SELECT  SQL_VARIANT_PROPERTY (199.96     ,'BaseType'),
    SQL_VARIANT_PROPERTY (199.96     ,'Precision'),
    SQL_VARIANT_PROPERTY (199.96     ,'Scale')

SELECT  SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'BaseType'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Precision'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Scale')

Результати:

числовий 5 2
числовий 38 1

Так 199.96є numeric(5,2)і довше Floor(Cast(etc))є numeric(38,1).

Ці правила результуючої точності і масштабу операції віднімання (тобто e1 - e2) виглядати наступним чином :

Точність: max (s1, s2) + max (p1-s1, p2-s2) + 1
Шкала: max (s1, s2)

Це оцінюється так:

Точність: макс. (1,2) + макс. (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
Шкала: макс. (1,2) => 2

Ви також можете скористатися посиланням на правила, щоб з’ясувати, звідки воно numeric(38,1)взялося (підказка: ви помножили два значення з точністю 19).

Але:

  • Точність результату та масштаб мають абсолютний максимум 38. Коли точність результату перевищує 38, вона зменшується до 38, а відповідна шкала зменшується для спроби запобігти усіченню цілісної частини результату. У деяких випадках, таких як множення або ділення, коефіцієнт масштабування не зменшується, щоб зберегти десяткову точність, хоча помилку переповнення можна збільшити.

На жаль Точність дорівнює 40. Ми повинні її зменшити, і оскільки зменшення точності завжди повинно відсікати найменш значущі цифри, що означає зменшення масштабу. Кінцевим результуючим типом виразу буде numeric(38,0), який для 199.96раундів до 200.

Можливо, ви можете це виправити, перемістивши та консолідувавши CAST()операції зсередини великого виразу до одного CAST() навколо всього результату виразу. Отже це:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

Стає:

SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))

Я навіть міг би зняти зовнішній склад.

Тут ми дізнаємось, що нам слід вибирати типи, які відповідають точності та масштабу, які ми маємо зараз , а не очікуваному результату. Немає сенсу просто обирати великі цифри точності, оскільки SQL Server буде мутувати ці типи під час арифметичних операцій, щоб уникнути переповнення.


Більше інформації:


20

Слідкуйте за типами даних, які стосуються такого твердження:

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))))
  1. NUMERIC(19, 4) * NUMERIC(19, 4)є NUMERIC(38, 7)(див. нижче)
    • FLOOR(NUMERIC(38, 7))є NUMERIC(38, 0)(див. нижче)
  2. 0.0 є NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0) є NUMERIC(38, 1)
  3. 199.96 є NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)є NUMERIC(38, 1)(див. нижче)

Це пояснює, чому ви отримуєте 200.0( одну цифру після десяткової, а не нульову ) замість 199.96.

Примітки:

FLOORповертає найбільше ціле число, менше або рівне вказаному числовому виразу, і результат має той самий тип, що і вхід. Він повертає INT для INT, FLOAT для FLOAT та NUMERIC (x, 0) для NUMERIC (x, y).

Відповідно до алгоритму :

Operation | Result precision                    | Result scale*
e1 * e2   | p1 + p2 + 1                         | s1 + s2
e1 - e2   | max(s1, s2) + max(p1-s1, p2-s2) + 1 | max(s1, s2)

* Точність результату та масштаб мають абсолютний максимум 38. Коли точність результату перевищує 38, її зменшують до 38, а відповідну шкалу зменшують, щоб не допустити усічення цілісної частини результату.

Опис також містить деталі того, як саме зменшується масштаб у операціях додавання та множення. На основі цього опису:

  • NUMERIC(19, 4) * NUMERIC(19, 4)є NUMERIC(39, 8)і затискаєтьсяNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)є NUMERIC(40, 1)і затискаєтьсяNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)є NUMERIC(40, 2)і затискаєтьсяNUMERIC(38, 1)

Ось моя спроба реалізувати алгоритм у JavaScript. Я перевірив результати порівняно з SQL Server. Це відповідає на саму суть Вашого запитання.

// https://docs.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql?view=sql-server-2017

function numericTest_mul(p1, s1, p2, s2) {
  // e1 * e2
  var precision = p1 + p2 + 1;
  var scale = s1 + s2;

  // see notes in the linked article about multiplication operations
  var newscale;
  if (precision - scale < 32) {
    newscale = Math.min(scale, 38 - (precision - scale));
  } else if (scale < 6 && precision - scale > 32) {
    newscale = scale;
  } else if (scale > 6 && precision - scale > 32) {
    newscale = 6;
  }

  console.log("NUMERIC(%d, %d) * NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_add(p1, s1, p2, s2) {
  // e1 + e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2) + 1;
  var scale = Math.max(s1, s2);

  // see notes in the linked article about addition operations
  var newscale;
  if (Math.max(p1 - s1, p2 - s2) > Math.min(38, precision) - scale) {
    newscale = Math.min(precision, 38) - Math.max(p1 - s1, p2 - s2);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_union(p1, s1, p2, s2) {
  // e1 UNION e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2);
  var scale = Math.max(s1, s2);

  // my idea of how newscale should be calculated, not official
  var newscale;
  if (precision > 38) {
    newscale = scale - (precision - 38);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

/*
 * first example in question
 */

// CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))
numericTest_mul(19, 4, 19, 4);

// 0.0 * FLOOR(...)
numericTest_mul(1, 1, 38, 0);

// 199.96 * ...
numericTest_add(5, 2, 38, 1);

/*
 * IIF examples in question
 * the logic used to determine result data type of IIF / CASE statement
 * is same as the logic used inside UNION operations
 */

// FLOOR(DECIMAL(38, 7)) UNION CAST(1999.96 AS DECIMAL(19, 4)))
numericTest_union(38, 0, 19, 4);

// CAST(1.0 AS DECIMAL (36, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(36, 0, 19, 4);

// CAST(1.0 AS DECIMAL (37, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(37, 0, 19, 4);

// CAST(1.0 AS DECIMAL (38, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(38, 0, 19, 4);

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