Por que o TSQL retorna o valor errado para POWER (2., 64.)?


14

select POWER(2.,64.)retorna em 18446744073709552000vez de 18446744073709551616. Parece ter apenas 16 dígitos de precisão (arredondando o dia 17).

Mesmo explicitando a precisão, select power(cast(2 as numeric(38,0)),cast(64 as numeric(38,0)))ele ainda retorna o resultado arredondado.

Isso parece ser uma operação bastante básica para que ele seja exibido arbitrariamente com 16 dígitos de precisão como este. O valor mais alto que ele pode calcular corretamente é apenas o que POWER(2.,56.)falhar POWER(2.,57.). O que está acontecendo aqui?

O que é realmente terrível é que 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.;realmente retorna o valor certo. Tanta coisa para a concisão.


Respostas:


17

Na documentação online :

POWER ( float_expression , y )  

Argumentos

float_expression É uma expressão do tipo float ou de um tipo que pode ser implicitamente convertido em float

A implicação é que tudo o que você passar como o primeiro parâmetro será implicitamente convertido em a float(53) antes que a função seja executada. No entanto, este não é (sempre?) O caso .

Se fosse o caso, explicaria a perda de precisão:

A conversão de valores flutuantes que usam notação científica em decimal ou numérico é restrita apenas a valores de precisão de 17 dígitos. Qualquer valor com precisão maior que 17 arredondamentos para zero.

Por outro lado, o literal 2.é do tipo numeric…:

DECLARE @foo sql_variant;
SELECT @foo = 2.;
SELECT SQL_VARIANT_PROPERTY(@foo, 'BaseType');
GO
| (Sem nome da coluna) |
| : --------------- |
| numérico |

dbfiddle aqui

… E o operador multiply retorna o tipo de dados do argumento com maior precedência .

Parece que em 2016 (SP1), toda a precisão é mantida:

SELECT @@version;
GO
| (Sem nome da coluna) |
| : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------- |
| Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) <br> 28 de outubro de 2016 18:17:30 <br> Copyright (c) Microsoft Corporation <br> Express Edition (64 bits) no Windows Server 2012 R2 Padrão 6.3 <X64> (Compilação 9600:) (Hypervisor) <br> |
SELECT POWER(2.,64.);
GO
| (Sem nome da coluna) |
| : ------------------- |
| 18446744073709551616 |

dbfiddle aqui

… Mas em 2014 (SP2), eles não são:

SELECT @@version;
GO
| (Sem nome da coluna) |
| : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------------------------------------ |
| Microsoft SQL Server 2014 (SP2) (KB3171021) - 12.0.5000.0 (X64) <br> 17 de junho de 2016 19:14:09 <br> Copyright (c) Microsoft Corporation <br> Express Edition (64 bits) no Windows NT 6.3 <X64> (Compilação 9600:) (Hypervisor) <br> |
SELECT POWER(2.,64.);
GO
| (Sem nome da coluna) |
| : ------------------- |
| 18446744073709552000 |

dbfiddle aqui


1
Então, basicamente, a função POWER é inútil para qualquer coisa que exija mais de 17 dígitos de precisão. É por isso que produz o resultado certo, POWER(2.,56.) = 72057594037927936mas não superior. Eu acho que vou ter que escrever minha própria função POWER que se multiplica em um loop, lol.
Triynko

14

O resultado de 2 64 é exatamente representável emfloat (e, realnesse caso).

O problema surge quando esse resultado preciso é convertido novamente em numeric (o tipo do primeiro POWERoperando).

Antes da introdução do nível 130 de compatibilidade de banco de dados, o SQL Server arredondado float para numericconversões implícitas para um máximo de 17 dígitos.

No nível de compatibilidade 130, o máximo de precisão possível é preservado durante a conversão. Isso está documentado no artigo da Base de Dados de Conhecimento:

Melhorias no SQL Server 2016 no tratamento de alguns tipos de dados e operações incomuns

Para tirar proveito disso no Banco de Dados SQL do Azure, você precisa definir o valor COMPATIBILITY_LEVEL130:

ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 130;

O teste de carga de trabalho é necessário porque o novo arranjo não é uma panacéia. Por exemplo:

SELECT POWER(10., 38);

... deve gerar um erro porque 10 38 não pode ser armazenado em numeric(precisão máxima de 38). Um erro de estouro resulta em compatibilidade com 120, mas o resultado em 130 é:

99999999999999997748809823456034029568 -- (38 digits)

2

Com um pouco de matemática, podemos encontrar uma solução alternativa. Para impar n:

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

Para pares n:

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

Uma maneira de escrever isso em T-SQL:

DECLARE @exponent INTEGER = 57;

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

Testado no SQL Server 2008, o resultado é 144115188075855872 em vez de 144115188075855870.

Isso funciona até um expoente de 113. Parece que um NUMÉRICO (38,0) pode armazenar até 2 ^ 126, portanto não há uma cobertura completa, mas a fórmula pode ser dividida em mais partes, se necessário .


0

Apenas por diversão, uma solução CTE recursiva:

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 ;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.