SQL str() против функции round()

#sql #rounding

#sql #округление

Вопрос:

При работе с функцией str() я обнаружил, что в некоторых случаях она округляется неправильно, в то время как функция round() работает должным образом. Пожалуйста, посмотрите на пример:

 declare @v decimal(18,2) = 29.95
select str(@v, 18, 1)
--29.9
select round(@v, 1)
--30.00

set @v = 39.95
select str(@v, 18, 1)
--40.00
select round(@v, 1)
--40.00
 

Кто-нибудь может объяснить, почему это происходит?

EDIT1: я протестировал различные обходные пути со следующим базовым кодом:

 declare @v decimal(18,2) = 9.95
declare @r varchar(100)
declare @c int = 1000000
declare @ms int
declare @dt datetime2

set @dt = sysdatetime()

while @c > 0
begin
    set @r = --different roundings
    set @c = @c - 1
end

set @ms = DATEDIFF(ms, @dt, sysdatetime())
select @ms, @r
 

Вариант 1 (исходный, в некоторых случаях округляется неправильно):

 str(@v, 18, 1)
 

Вариант 2 (слегка измененный, но округляется правильно):

 str(round(@v, 1), 18, 1)
 

Вариант 3 (двойное преобразование и округление):

 convert(varchar(20), convert(decimal(18,1), round(@v, 1)))
 

Вариант 4 (только двойное преобразование):

 convert(varchar(20), convert(decimal(18,1), @v))
 

Результаты:
Варианты 1 и 2 примерно в 2 раза медленнее, чем два последних, но результат выравнивается по правому краю. Самым быстрым является вариант 4.

Комментарии:

1. Что каждый специалист по информатике должен знать об арифметике с плавающей запятой

2. Спасибо! Это действительно хорошая статья!

Ответ №1:

Параметр для str() является плавающим, поэтому ваше десятичное значение неявно преобразуется в значение с плавающей точкой (53), которое затем преобразуется в строку. Таким образом, вы видите ошибку округления с плавающей запятой.

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

 declare @v decimal(18,2) = 29.95
select top(1) str(@v, 18, 1)
 
 <ScalarOperator ScalarString="str(CONVERT_IMPLICIT(float(53),[@v],0),(18),(1))">
 

Комментарии:

1. Как мне использовать последнюю строку кода, которую вы написали? И каким будет обходной путь, чтобы я получил правильно округленный varchar с максимально возможной производительностью?

2. @DmitryTimofeev Вы не должны использовать последнюю строку. Это ответ на ваш вопрос «почему это происходит?».

3. Я на самом деле пришел к тому же самому :), просто подумал, может быть, есть лучшее решение.

4. @DmitryTimofeev Так — то лучше cast(cast(round(@v, 1) as decimal(18, 1)) as varchar(10)) . Это позволяет полностью избежать преобразований с плавающей запятой,

5. Разве это не было бы еще лучше: convert(varchar, convert(decimal(18,1), @v))

Ответ №2:

Синтаксис STR(); STR ( float_expression [ , длина [ , десятичное число ] ] ) ясно говорит о том, что число является float_expression . Поэтому, какое бы число вы ни указали, оно сначала будет преобразовано в число с плавающей запятой (n), где значение по умолчанию n = 53.

Итак

ВЫБЕРИТЕ STR(4.65,5,1), ВЫБЕРИТЕ STR(3.65,5,1)

Равный:

ВЫБЕРИТЕ STR(CAST(4.65 КАК FLOAT(53)),5,1) , STR(CAST(3.65 КАК FLOAT(53)),5,1)

Если вы укажете n, скажем, n = 4, это даст ответ, который вы ожидаете (т. Е.; 4.7 и 3.7)

ВЫБЕРИТЕ STR(ПРИВЕДЕНИЕ(4.65 КАК ЗНАЧЕНИЕ С ПЛАВАЮЩЕЙ ЗАПЯТОЙ(4)),5,1) , STR(ПРИВЕДЕНИЕ(3,65 В КАЧЕСТВЕ ПОПЛАВКА(4)),5,1) —4.7, 3.7

Комментарии:

1. Это по-прежнему дает неверный результат: объявить @v десятичное число (18,2) = 9,95 ВЫБЕРИТЕ STR(ПРИВЕДЕНИЕ (@v КАК FLOAT(53)),5,1) ВЫБЕРИТЕ STR(ПРИВЕДЕНИЕ (@v КАК FLOAT(4)),5,1)