Почему в SQL 199.96 - 0 = 200?

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....

Кто-нибудь знает, что, черт возьми, здесь происходит? Я имею в виду, что это определенно как-то связано с типом данных decimal, но я не могу осмыслить это ...


Было много недоразумений относительно того, к какому типу данных относятся числовые литералы, поэтому я решил показать реальную строку:

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)))

Какого черта - floor все равно должен возвращать целое число. Что тут происходит? :-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. Все эти приведения и минимальные значения с неявным преобразованием в числа с плавающей запятой и обратно гарантированно приведут к потере точности.
Panagiotis Kanavos
1
@Silverdust, только если он пришел со стола. Буквально в выражении это, вероятно,float
Панайотис Канавос
1
О ... и Floor()вовсе не возвращать int. Он возвращает тот же тип, что и исходное выражение , с удаленной десятичной частью. В остальном IIF()функция возвращает тип с наивысшим приоритетом ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Итак, во втором примере, где вы приводите к int, более высокий приоритет - это простое приведение к числовому (19,4).
Joel Coehoorn
1
Отличный ответ (кто знал, что вы можете изучить метаданные варианта sql?), Но в 2012 году я получаю ожидаемые результаты (199,96).
benjamin moskovits
2
Я не слишком знаком с MS SQL, но должен сказать, что просмотр всех этих операций приведения и т. Д. Быстро привлек мое внимание ... поэтому я должен связать это, потому что никто никогда не должен использовать floatтипы точек входа для обработки валюты .
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);

Салман А
источник