¿Por qué TSQL devuelve el valor incorrecto para POWER (2., 64)?

14

select POWER(2.,64.)vuelve en 18446744073709552000lugar de 18446744073709551616. Parece tener solo 16 dígitos de precisión (redondeando el 17).

Incluso haciendo explícita la precisión select power(cast(2 as numeric(38,0)),cast(64 as numeric(38,0))), aún devuelve el resultado redondeado.

Esto parece una operación bastante básica para que se descarte arbitrariamente a 16 dígitos de precisión como esta. Lo más alto que puede calcular correctamente es solo POWER(2.,56.), fallando POWER(2.,57.). ¿Que esta pasando aqui?

Lo que es realmente terrible es que en 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.;realidad devuelve el valor correcto. Demasiado para la brevedad.

Triynko
fuente

Respuestas:

17

De la documentación en línea :

POWER ( float_expression , y )  

Argumentos

expresión_ float Es una expresión de tipo float o de un tipo que se puede convertir implícitamente en float

La implicación es que lo que pase como primer parámetro se convertirá implícitamente en a float(53) antes de que se ejecute la función. Sin embargo, este no es (¿siempre?) El caso .

Si fuera el caso, explicaría la pérdida de precisión:

La conversión de valores flotantes que usan notación científica a decimal o numérico está restringida a valores de precisión de 17 dígitos solamente. Cualquier valor con precisión superior a 17 rondas a cero.

Por otro lado, el literal 2.es tipo numeric...:

DECLARE @foo sql_variant;
SELECT @foo = 2.;
SELECT SQL_VARIANT_PROPERTY(@foo, 'BaseType');
GO
El | (Sin nombre de columna) |
El | : --------------- |
El | numérico |

dbfiddle aquí

... y el operador de multiplicación devuelve el tipo de datos del argumento con mayor precedencia .

Parece que en 2016 (SP1), se conserva toda la precisión:

SELECT @@version;
GO
El | (Sin nombre de columna) |
El | : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------- |
El | Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) <br> 28 de octubre de 2016 18:17:30 <br> Copyright (c) Microsoft Corporation <br> Express Edition (64 bits) en Windows Server 2012 R2 Standard 6.3 <X64> (Build 9600:) (Hypervisor) <br> |
SELECT POWER(2.,64.);
GO
El | (Sin nombre de columna) |
El | : ------------------- |
El | 18446744073709551616 |

dbfiddle aquí

... pero en 2014 (SP2), no son:

SELECT @@version;
GO
El | (Sin nombre de columna) |
El | : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------------------------------------ |
El | Microsoft SQL Server 2014 (SP2) (KB3171021) - 12.0.5000.0 (X64) <br> 17 de junio de 2016 19:14:09 <br> Copyright (c) Microsoft Corporation <br> Express Edition (64 bits) en Windows NT 6.3 <X64> (Build 9600:) (Hypervisor) <br> |
SELECT POWER(2.,64.);
GO
El | (Sin nombre de columna) |
El | : ------------------- |
El | 18446744073709552000 |

dbfiddle aquí

Jack dice que intente topanswers.xyz
fuente
1
Básicamente, la función POWER no sirve para nada que requiera más de 17 dígitos de precisión. Es por eso que produce el resultado correcto POWER(2.,56.) = 72057594037927936pero no superior. Supongo que tendré que escribir mi propia función POWER que simplemente se multiplica en un bucle, jajaja.
Triynko
14

El resultado de 2 64 es exactamente representable enfloat (y realpara el caso).

El problema surge cuando este resultado preciso se convierte de nuevo a numeric (el tipo del primer POWERoperando).

Antes de que se introdujera el nivel de compatibilidad de la base de datos 130, SQL Server redondeó float a numericconversiones implícitas a un máximo de 17 dígitos.

Bajo el nivel de compatibilidad 130, se conserva la mayor precisión posible durante la conversión. Esto está documentado en el artículo de Knowledge Base:

Mejoras de SQL Server 2016 en el manejo de algunos tipos de datos y operaciones poco comunes

Para aprovechar esto en Azure SQL Database, debe establecer COMPATIBILITY_LEVEL130 en:

ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 130;

La prueba de carga de trabajo es necesaria porque el nuevo arreglo no es una panacea. Por ejemplo:

SELECT POWER(10., 38);

... debería arrojar un error porque no se puede almacenar 10 38numeric (precisión máxima de 38). Un error de desbordamiento da como resultado una compatibilidad inferior a 120, pero el resultado por debajo de 130 es:

99999999999999997748809823456034029568 -- (38 digits)
Paul White 9
fuente
2

Con un poco de matemática podemos encontrar una solución. Por impar n:

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

Para incluso n:

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

Una forma de escribir eso en T-SQL:

DECLARE @exponent INTEGER = 57;

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

Probado en SQL Server 2008, el resultado es 144115188075855872 en lugar de 144115188075855870.

Esto funciona hasta un exponente de 113. Parece que un NUMÉRICO (38,0) puede almacenar hasta 2 ^ 126, por lo que no hay una cobertura completa, pero la fórmula podría dividirse en más partes si es necesario .

Joe Obbish
fuente
0

Solo por diversión, una solución recursiva de 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 ;
ypercubeᵀᴹ
fuente