Warum gibt TSQL für POWER (2., 64.) Den falschen Wert zurück?

14

select POWER(2.,64.)kehrt zurück 18446744073709552000statt 18446744073709551616. Es scheint nur 16 Stellen Genauigkeit zu haben (Rundung des 17.).

Selbst wenn die Genauigkeit explizit angegeben wird select power(cast(2 as numeric(38,0)),cast(64 as numeric(38,0))), wird das gerundete Ergebnis zurückgegeben.

Dies scheint eine ziemlich grundlegende Operation zu sein, die willkürlich mit einer Genauigkeit von 16 Stellen abbricht. Das Höchste, das es richtig berechnen kann, ist nur POWER(2.,56.), wenn es fehlschlägt POWER(2.,57.). Was geht hier vor sich?

Was wirklich schrecklich ist, ist, dass 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.;tatsächlich der richtige Wert zurückgegeben wird. Soviel zur Knappheit.

Triynko
quelle

Antworten:

17

Aus der Online-Dokumentation :

POWER ( float_expression , y )  

Argumente

float_expression Ist ein Ausdruck vom Typ float oder von einem Typ , der implizit in float konvertiert werden kann

Die Implikation ist, dass alles, was Sie als ersten Parameter übergeben, implizit in a umgewandelt wird, float(53) bevor die Funktion ausgeführt wird. Dies ist jedoch nicht (immer?) Der Fall .

Wenn dies der Fall wäre, würde dies den Präzisionsverlust erklären:

Die Konvertierung von Float-Werten in Dezimalzahlen oder Zahlen mit wissenschaftlicher Schreibweise ist auf Werte mit einer Genauigkeit von nur 17 Stellen beschränkt. Jeder Wert mit einer Genauigkeit von mehr als 17 Runden auf Null.

Auf der anderen Seite ist das Literal 2.Typ numeric…:

DECLARE @foo sql_variant;
SELECT @foo = 2.;
SELECT SQL_VARIANT_PROPERTY(@foo, 'BaseType');
GO
| (Kein Spaltenname) |
| : --------------- |
| numerisch |

dbfiddle hier

… Und der Multiplikationsoperator gibt den Datentyp des Arguments mit der höheren Priorität zurück .

Es scheint, dass im Jahr 2016 (SP1) die gesamte Präzision erhalten bleibt:

SELECT @@version;
GO
| (Kein Spaltenname) |
| : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------- |
| Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) - 28. Oktober 2016, 18:17:30 Uhr <br> Copyright (c) Microsoft Corporation Express Edition (64-Bit) unter Windows Server 2012 R2 Standard 6.3 <X64> (Build 9600:) (Hypervisor) <br> |
SELECT POWER(2.,64.);
GO
| (Kein Spaltenname) |
| : ------------------- |
| 18446744073709551616 |

dbfiddle hier

… Aber im Jahr 2014 (SP2) sind sie nicht:

SELECT @@version;
GO
| (Kein Spaltenname) |
| : ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------------------------------------ |
| Microsoft SQL Server 2014 (SP2) (KB3171021) - 12.0.5000.0 (X64) - 17. Juni 2016, 19:14:09 Uhr <br> Copyright (c) Microsoft Corporation Express Edition (64-Bit) unter Windows NT 6.3 <X64> (Build 9600:) (Hypervisor) <br> |
SELECT POWER(2.,64.);
GO
| (Kein Spaltenname) |
| : ------------------- |
| 18446744073709552000 |

dbfiddle hier

Jack Douglas
quelle
1
Im Grunde genommen ist die POWER-Funktion für alles nutzlos, was eine Genauigkeit von mehr als 17 Stellen erfordert. Deshalb liefert es das richtige Ergebnis für POWER(2.,56.) = 72057594037927936aber kein höheres. Ich denke, ich muss meine eigene POWER-Funktion schreiben, die sich einfach in einer Schleife multipliziert, lol.
Triynko
14

Das Ergebnis von 2 64 ist in float(und realfür diesen Fall) genau darstellbar .

Das Problem tritt auf, wenn dieses genaue Ergebnis zurück in numeric(den Typ des ersten POWEROperanden) konvertiert wird .

Vor Einführung der Datenbankkompatibilitätsstufe 130 wurde SQL Server floatauf numericimplizite Konvertierungen mit maximal 17 Stellen gerundet .

Unter der Kompatibilitätsstufe 130 bleibt bei der Konvertierung so viel Präzision wie möglich erhalten. Dies ist im Knowledge Base-Artikel dokumentiert:

SQL Server 2016-Verbesserungen bei der Behandlung einiger Datentypen und ungewöhnlicher Vorgänge

Um dies in der Azure SQL-Datenbank zu nutzen, müssen Sie den Wert COMPATIBILITY_LEVELauf 130 festlegen :

ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 130;

Workload-Tests sind erforderlich, da die neue Regelung kein Allheilmittel darstellt. Beispielsweise:

SELECT POWER(10., 38);

... sollte einen Fehler auslösen, da 10 38 nicht gespeichert werden kann numeric(maximale Genauigkeit von 38). Ein Überlauffehler führt zu einer Kompatibilität unter 120, aber das Ergebnis unter 130 ist:

99999999999999997748809823456034029568 -- (38 digits)
Paul White Monica wieder einsetzen
quelle
2

Mit ein wenig Mathe können wir einen Workaround finden. Für ungerade n:

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

Für noch n:

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

Eine Möglichkeit, das in T-SQL zu schreiben:

DECLARE @exponent INTEGER = 57;

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

Getestet unter SQL Server 2008 lautet das Ergebnis 144115188075855872 anstelle von 144115188075855870.

Dies funktioniert bis zu einem Exponenten von 113. Es sieht so aus, als ob eine NUMERISCHE Zahl (38,0) bis zu 2 ^ 126 speichern kann, sodass keine vollständige Abdeckung vorliegt. Die Formel kann jedoch bei Bedarf in mehrere Teile aufgeteilt werden .

Joe Obbish
quelle
0

Nur zum Spaß eine rekursive CTE-Lösung:

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ᵀᴹ
quelle