Sql округляет до целого как исправить

Доброго дня, хабровчане! Пришлось мне в проекте столкнуться с точностью вычислений в MS SQL Server и я обнаружил не совсем интуитивное поведение при выполнении к...

Время прочтения
2 мин

Просмотры 33K

Доброго дня, хабровчане! Пришлось мне в проекте столкнуться с точностью вычислений в MS SQL Server и я обнаружил не совсем интуитивное поведение при выполнении казалось бы интуитивных операций.

Для затравки вопрос (попробуйте ответить на него, не выполняя):
Каков будет результат операции?

declare @var1 decimal(38,10) = 0.0000007,
        @var2 decimal(38,10) = 1;
select @var1 * @var2;

Ответ и объяснение под катом

Итак, сначала ответ: 0.000001

Какого?

На самом деле ответ достаточно прост, но от этого не легче. Все дело в том, что при выполнении арифметических операций с десятичными числами результат может быть сильно больше исходных значений, например, если умножить 10^6 и 10^6, то получим 10^12. Это аж на 6 разрядов больше, чем исходные значения. Аналогично и с делением. Поэтому MS SQL при вычислении типа результирующего выражения применяет следующие правила:

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)

* precision и scale результата имеют абсолютный максимум, равный 38. Если значение precision превышает 38, то соответствующий scale уменьшается, чтобы по возможности предотвратить усечение интегральной части результата.

В документации нет подробного описания как происходит округление и до каких пределов, но экспериментально у меня не получилось достичь округления больше, чем decimal(38,6).

Отсюда и результат выражения в начале: 0.000001 ровно 6 знаков после запятой. Чтобы не быть голословным, выполним следующий запрос:

declare @var1 decimal(38,10) = 0.0000007,
        @var2 decimal(38,10) = 1,
        @res sql_variant;
set @res = @var1 * @var2;
select @res, 
       SQL_VARIANT_PROPERTY(@res, 'BaseType') as BaseType, 
       SQL_VARIANT_PROPERTY(@res, 'Precision') as Precision,
       SQL_VARIANT_PROPERTY(@res, 'Scale') as Scale;

Получим следующий результат:

res BaseType Precision Scale
0.000001 decimal 38 6

Как же с этим жить?

Придется с этим смириться и всегда (абсолютно всегда!) очень деликатно выставлять точность. В нашем случае вот такой скрипт вернет ожидаемый результат:

declare @var1 decimal(18,10) = 0.0000007,
        @var2 decimal(18,10) = 1,
        @res sql_variant;
set @res = @var1 * @var2;
select @res, 
       SQL_VARIANT_PROPERTY(@res, 'BaseType') as BaseType, 
       SQL_VARIANT_PROPERTY(@res, 'Precision') as Precision,
       SQL_VARIANT_PROPERTY(@res, 'Scale') as Scale;

res BaseType Precision Scale
0.00000070000000000000 decimal 37 20

Вместо послесловия

И на последок еще немного sql-магии. Что будет в результате выполнения вот такого скрипта:

declare @var1 decimal(38,10) = 0.0000007,
        @var2 int = 1;
select @var1 * @var2, @var1 * 1;

Ответ

@var1 *
@var2 = 0.000001
@var1 * 1 = 0.00000070

PS: так же надо быть внимательным с операциями аггрегации, потому что они тоже меняют точность результата. Вот хорошая статья, описывающая проблему.

Я использую SQL Server Management Studio и имею следующую схему:

CREATE TABLE tmp(
    id int NOT NULL IDENTITY(1,1)PRIMARY KEY,
    toleranceRegion DECIMAL
)

Затем выполняю следующие прошивки:

INSERT INTO tmp VALUES(3.2); 
INSERT INTO tmp VALUES(5.678);
INSERT INTO tmp VALUES(1.95);

Ожидаемый результат:

id  toleranceRegion
--  ---------------
1   3.2
2   5.678
3   1.95

Фактический выход:

id  toleranceRegion
--  ---------------
1   3
2   6
3   2

Почему вставленные значения поля допуска округляются до ближайшего целого числа?

3 ответа

Лучший ответ

Вы не определили масштаб / точность для десятичной дроби. Если вам нужно 3 цифры после десятичной дроби, вы должны определить ее как DECIMAL (9,3), что даст вам 6 разрядов перед десятичной дробью и десятичную дробь до 3 разрядов. Вам необходимо проанализировать ожидаемые данные и, исходя из ваших ожиданий, указать правильную точность и масштаб для определения столбца.

CREATE TABLE tmp(
    id int NOT NULL IDENTITY(1,1)PRIMARY KEY,
    toleranceRegion DECIMAL(9,3)
)

См. Документацию по серверу Sql для decimal здесь.


20

Igor
27 Июн 2016 в 20:33

Это связано с тем, что вы не устанавливаете масштаб , что означает, что система использует масштаб по умолчанию, равный нулю:

s (масштаб) Количество десятичных цифр, которые будут сохранены справа от десятичной точки. Это число вычитается из p, чтобы определить максимальное количество цифр слева от десятичной точки. Максимальное количество десятичных цифр, которое может храниться справа от десятичной точки. Масштаб должен иметь значение от 0 до p. Масштаб можно указать, только если указана точность. Масштаб по умолчанию — 0 . (курсив мой)

Другими словами, SQL Server хранит нулевые цифры справа от десятичной точки.


4

Sergey Kalinichenko
27 Июн 2016 в 20:33

Установите свою точность

Десятичный (18,4)

Это будут десятичные дроби


3

John Cappelletti
27 Июн 2016 в 20:33

Функция ROUND используется для округления дробей до нужного знака в дробной части.

См. также функцию truncate,
которая усекает числа до нужного знака.

См. также функцию CEILING,
которая округляет дроби в большую сторону.

См. также функцию FLOOR,
которая округляет дроби в меньшую сторону.

Синтаксис

Округление до целого:

SELECT ROUND(поле) FROM имя_таблицы WHERE условие

Округление до определенного знака в дробной части:

SELECT ROUND(поле, сколько_знаков_оставить) FROM имя_таблицы WHERE условие

Примеры

Все примеры будут по этой таблице workers, если не сказано иное:

id
айди
name
имя
age
возраст
salary
зарплата
1 Дима 23 100.129
2 Петя 24 200.391
3 Вася 25 300.934

Пример

В данном примере дробная зарплата округляется до целых при выборке:

SELECT *, ROUND(salary) as salary FROM workers

SQL запрос выберет следующие строки:

id
айди
name
имя
age
возраст
salary
зарплата
1 Дима 23 100
2 Петя 24 200
3 Вася 25 301

Пример

В данном примере дробная зарплата округляется до двух знаков после точки:

SELECT *, ROUND(salary, 2) as salary FROM workers

SQL запрос выберет следующие строки:

id
айди
name
имя
age
возраст
salary
зарплата
1 Дима 23 100.13
2 Петя 24 200.39
3 Вася 25 300.93

Пример

В данном примере дробная зарплата округляется до одного знака после точки:

SELECT *, ROUND(salary, 1) as salary FROM workers

SQL запрос выберет следующие строки:

id
айди
name
имя
age
возраст
salary
зарплата
1 Дима 23 100.1
2 Петя 24 200.4
3 Вася 25 300.9

Пример

В данном примере дробная зарплата округляется до целых (0 соответствует целому числу):

SELECT *, ROUND(salary, 0) as salary FROM workers

SQL запрос выберет следующие строки:

id
айди
name
имя
age
возраст
salary
зарплата
1 Дима 23 100
2 Петя 24 200
3 Вася 25 301

Понравилась статья? Поделить с друзьями:
  • Sql server ошибка 15517
  • Sql server error 15404
  • Sql server 2008 r2 setup has encountered an error
  • Sql error state 42s22
  • Sql error ora 00984 употребление столбца здесь недопустимо 00984 00000 column not allowed here