Вопрос округления номера TSQL

У меня есть кусок кода:

IF OBJECT_ID(N'dbo.rounding_testing') IS NOT NULL
    DROP FUNCTION dbo.rounding_testing;
GO
CREATE FUNCTION dbo.rounding_testing
(
    @value FLOAT,
    @digit INT
)
RETURNS FLOAT
BEGIN
    DECLARE
        @factor FLOAT,
        @result FLOAT;
    SELECT @factor = POWER(10, @digit);

    SELECT @result = FLOOR(@value * @factor + 0.4);

    RETURN @result;
END;
GO

SELECT dbo.rounding_testing(5.7456, 3);
SELECT FLOOR(5.7456 * 1000 + 0.4);

Результаты:

5745
5746

Я ожидаю два 5746. Я попытался отладить функцию и обнаружил интересное поведение. Ниже приведены некоторые тесты, которые я сделал в Immediate Window при отладке.

@factor
1.000000000000000e+003
@result
5.745000000000000e+003
@value
5.745600000000000e+000
@value*@factor
5745.6
@value*@factor+0.4
5746
floor(@value*@factor+0.4)
5745
floor(5746)
5746

Кто-нибудь может помочь объяснить результат? Особенно эти три строки:

@value*@factor+0.4
5746
floor(@value*@factor+0.4)
5745
floor(5746)
5746

2 ответа

Решение

В выражении FLOOR(5.7456 * 1000 + 0.4);часть между скобками оценивается первой. Для констант типы данных выводятся на основе записи; для 5.7456, то есть decimal(5,4); 1000 это int; а 0,4 это decimal(1,1), Предполагаемый тип данных для 5.7456 * 1000 decimal(10,4); и для полного выражения это decimal(11,4), Это все точные числовые типы данных, поэтому вы не будете испытывать никакого округления; конечный результат - 5746,0000 точно. FLOOR функция обрезает дробь и преобразует в decimal(11,0), вернувшись 5746.

В пользовательской функции вы сохраняете входные параметры и промежуточные результаты в float тип данных (данные с плавающей запятой). Этот тип данных предназначен для использования для приблизительных данных, таких как измерения, когда данные, которые вы читаете из инструмента, уже являются приблизительными. В старших классах я научился читать столько цифр, сколько смогу, но относился к последним как к незначительным - мне приходилось сохранять его во всех вычислениях, но округлять конечный результат до числа значащих цифр, основанных на точности моих измерений, Округление гарантирует, что неточности в последних цифрах не повлияют на конечный результат. Типы данных с плавающей точкой должны обрабатываться одинаково.

Внутри цифры с плавающей запятой представлены в системе счисления base-2. Это означает, что есть числа, которые имеют точное представление в нашей обычно используемой системе base-10 (например, 5.7456), но не имеют конечной дробной части в base-2. (Подобно тому, как, например, одна треть, которая может быть представлена ​​точно в base-3, имеет нескончаемую дробную часть в base-10: 0.33333333333(и т. Д.)). Количество цифр base-2, используемых для хранения float число является конечным, поэтому оно должно быть обрезано в конце, что приводит к тому, что оно округляется либо в большую, либо в меньшую сторону. Вы можете увидеть это, если запустите:

DECLARE @a float = 5.7456;
SELECT CAST(@a AS decimal(19,16));

В этом случае эффект отсечения после большого количества цифр base-2 заключается в том, что сохраненное значение на 0,0000000000000004 меньше, чем введенное десятичное значение. Эта небольшая разница превращается в огромный эффект из-за FLOOR функция, которая делает именно то, что должна делать: округлять до ближайшего целого числа.

(Я видел, что многие люди называют это ошибкой. Это не так. Это целенаправленное и задокументированное поведение. И потеря точности здесь не хуже и не лучше, чем потеря точности, которую вы получаете, сохраняя треть в DECIMAL(7,6); это немного менее очевидно, потому что мы все привыкли работать в base-10)

Вы можете исправить проблему, изменив значение float на real

IF OBJECT_ID(N'dbo.rounding_testing') IS NOT NULL
    DROP FUNCTION dbo.rounding_testing;
GO
CREATE FUNCTION dbo.rounding_testing
(
    @value REAL,
    @digit INT
)
RETURNS REAL
BEGIN
    DECLARE
        @factor REAL,
        @result REAL;
    SELECT @factor = POWER(10, @digit);

    SELECT @result = FLOOR(@value * @factor + 0.4);

    RETURN @result;
END;
GO

SELECT dbo.rounding_testing(5.7456, 3);
SELECT FLOOR(5.7456 * 1000 + 0.4);
sum(convert(int,<your column) * .01) as 'Decimal Amount'

Преобразуйте столбец в целое число, затем умножьте на 0,01. Поле суммы преобразования, если необходимо.

Другие вопросы по тегам