Почему TSQL возвращает неправильное значение для POWER (2., 64.)?


14

select POWER(2.,64.)возвращается 18446744073709552000вместо 18446744073709551616. Кажется, он имеет только 16 цифр точности (округляя до 17-го).

Даже делая точность явной, select power(cast(2 as numeric(38,0)),cast(64 as numeric(38,0)))она все равно возвращает округленный результат.

Это кажется довольно простой операцией для произвольного отслаивания с точностью до 16 цифр. Наивысшая можно правильно рассчитать только POWER(2.,56.), стенает POWER(2.,57.). Что здесь происходит?

Что действительно ужасно, так это то, что на select 2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.*2.;самом деле возвращает правильное значение. Так много для краткости.


Ответы:


17

Из онлайн-документации :

POWER ( float_expression , y )  

аргументы

float_expression Выражение типа float или типа, которое можно неявно преобразовать в float.

Подразумевается, что все, что вы передадите в качестве первого параметра, будет неявно приведено к float(53) перед выполнением функции. Однако это не (всегда?) Случай .

Если бы это было так, это объяснило бы потерю точности:

Преобразование значений с плавающей запятой, использующих научную запись, в десятичную или числовую форму ограничено значениями точности только 17 цифр. Любое значение с точностью выше 17 раундов до нуля.

С другой стороны, литерал 2.типа numeric...

DECLARE @foo sql_variant;
SELECT @foo = 2.;
SELECT SQL_VARIANT_PROPERTY(@foo, 'BaseType');
GO
| (Без названия столбца) |
| : --------------- |
| числовой |

dbfiddle здесь

… И оператор умножения возвращает тип данных аргумента с более высоким приоритетом .

Похоже, что в 2016 году (SP1) вся точность сохраняется:

SELECT @@version;
GO
| (Без названия столбца) |
| : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------- |
| Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) - 28 октября 2016 г. 18:17:30 <br> Авторское право (c) Корпорация Майкрософт, выпуск Express Edition (64-разрядная версия) на Windows Server 2012 R2 Standard 6.3 <X64> (сборка 9600:) (гипервизор) <br> |
SELECT POWER(2.,64.);
GO
| (Без названия столбца) |
| : ------------------- |
| 18446744073709551616 |

dbfiddle здесь

… Но в 2014 году (SP2) они не являются:

SELECT @@version;
GO
| (Без названия столбца) |
| : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------------------------------------ |
| Microsoft SQL Server 2014 (SP2) (KB3171021) - 12.0.5000.0 (X64) <br> 17 июня 2016 г. 19:14:09 <br> Авторские права (c) Корпорация Microsoft, экспресс-выпуск (64-разрядная версия) в Windows NT 6.3 <X64> (сборка 9600:) (гипервизор) <br> |
SELECT POWER(2.,64.);
GO
| (Без названия столбца) |
| : ------------------- |
| 18446744073709552000 |

dbfiddle здесь


1
Таким образом, в основном, функция POWER бесполезна для чего-либо, требующего более 17 цифр точности. Вот почему это дает правильный результат, POWER(2.,56.) = 72057594037927936но не выше. Я думаю, мне придется написать свою собственную функцию POWER, которая просто умножается в цикле, смеется.
Трийнко

14

Результат 2 64 точно представлен в floatrealв этом отношении).

Проблема возникает, когда этот точный результат преобразуется обратно в numeric(тип первого POWERоперанда).

До введения уровня совместимости базы данных 130 SQL Server округлялся floatдо numericнеявных преобразований до 17 цифр.

При уровне совместимости 130 максимально возможная точность сохраняется во время преобразования. Это задокументировано в статье базы знаний:

Улучшения SQL Server 2016 в обработке некоторых типов данных и необычных операций

Чтобы воспользоваться этим в базе данных SQL Azure, необходимо установить COMPATIBILITY_LEVELзначение 130:

ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 130;

Тестирование рабочей нагрузки необходимо, потому что новая схема не является панацеей. Например:

SELECT POWER(10., 38);

... должен выдать ошибку, потому что 10 38 не может быть сохранен в numeric(максимальная точность 38). Ошибка переполнения приводит к несовместимости 120, но результат ниже 130:

99999999999999997748809823456034029568 -- (38 digits)

2

С небольшой математикой мы можем найти обходной путь. Для странных n:

2 ^ n 
= 2 ^ (2k + 1)
= 2 * (2 ^ 2k)
= 2 * (2 ^ k) * (2 ^ k)

Для четных n:

2 ^ n 
= 2 ^ (2k)
= 1 * (2 ^ 2k)
= 1 * (2 ^ k) * (2 ^ k)

Один из способов написать это в T-SQL:

DECLARE @exponent INTEGER = 57;

SELECT (1 + @exponent % 2) * POWER(2., FLOOR(0.5 * @exponent)) * POWER(2., FLOOR(0.5 * @exponent));

Протестировано на SQL Server 2008, результат 144115188075855872 вместо 144115188075855870.

Это работает вплоть до показателя степени 113. Похоже, что NUMERIC (38,0) может хранить до 2 ^ 126, поэтому не совсем полное покрытие, но формула может быть разбита на несколько частей при необходимости ,


0

Просто для удовольствия, рекурсивное решение CTE:

with 
  prm (p, e) as           -- parameters, to evaluate: p**e
    (select 2, 64),       -- (2 ** 64)  
  pow (power, exp) as 
    (select cast(p as numeric(30,0)), 
            e
     from prm 
     union all 
     select cast(power * power * (case when exp % 2 = 0 then 1 else p end) 
                 as numeric(30,0)), 
            exp / 2 
     from prm, pow 
     where exp > 1 
    ) 
select power 
from pow 
where exp = 1 ;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.