Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Rundungsproblem in LOG- und EXP-Funktionen

In reinem T-SQL LOG und EXP arbeiten mit dem float Typ (8 Bytes), der nur 15-17 signifikante Stellen hat . Selbst die letzte 15. Stelle kann ungenau werden, wenn Sie ausreichend große Werte summieren. Ihre Daten sind numeric(22,6) , also reichen 15 signifikante Stellen nicht aus.

POWER kann numeric zurückgeben Typ mit möglicherweise höherer Genauigkeit, aber es ist für uns von geringem Nutzen, da sowohl LOG und LOG10 kann nur float zurückgeben sowieso.

Um das Problem zu demonstrieren, ändere ich den Typ in Ihrem Beispiel in numeric(15,0) und verwenden Sie POWER statt EXP :

DECLARE @TEST TABLE
  (
     PAR_COLUMN INT,
     PERIOD     INT,
     VALUE      NUMERIC(15, 0)
  );

INSERT INTO @TEST VALUES 
(1,601,10 ),
(1,602,20 ),
(1,603,30 ),
(1,604,40 ),
(1,605,50 ),
(1,606,60 ),
(2,601,100),
(2,602,200),
(2,603,300),
(2,604,400),
(2,605,500),
(2,606,600);

SELECT *,
    POWER(CAST(10 AS numeric(15,0)),
        Sum(LOG10(
            Abs(NULLIF(VALUE, 0))
            ))
        OVER(PARTITION BY PAR_COLUMN ORDER BY PERIOD)) AS Mul
FROM @TEST;

Ergebnis

+------------+--------+-------+-----------------+
| PAR_COLUMN | PERIOD | VALUE |       Mul       |
+------------+--------+-------+-----------------+
|          1 |    601 |    10 |              10 |
|          1 |    602 |    20 |             200 |
|          1 |    603 |    30 |            6000 |
|          1 |    604 |    40 |          240000 |
|          1 |    605 |    50 |        12000000 |
|          1 |    606 |    60 |       720000000 |
|          2 |    601 |   100 |             100 |
|          2 |    602 |   200 |           20000 |
|          2 |    603 |   300 |         6000000 |
|          2 |    604 |   400 |      2400000000 |
|          2 |    605 |   500 |   1200000000000 |
|          2 |    606 |   600 | 720000000000001 |
+------------+--------+-------+-----------------+

Hier verliert jeder Schritt an Präzision. Die Berechnung von LOG verliert an Genauigkeit, SUM verliert an Genauigkeit, EXP/POWER verliert an Genauigkeit. Ich glaube nicht, dass Sie mit diesen eingebauten Funktionen viel dagegen tun können.

Die Antwort lautet also:Verwenden Sie CLR mit C# decimal Typ (nicht double ), was eine höhere Genauigkeit unterstützt (28–29 signifikante Stellen). Ihr ursprünglicher SQL-Typ numeric(22,6) würde dazu passen. Und den Trick mit LOG/EXP bräuchte man nicht .

Hoppla. Ich habe versucht, ein CLR-Aggregat zu erstellen, das Product berechnet. Es funktioniert in meinen Tests, aber nur als einfaches Aggregat, d.h.

Das funktioniert:

SELECT T.PAR_COLUMN, [dbo].[Product](T.VALUE) AS P
FROM @TEST AS T
GROUP BY T.PAR_COLUMN;

Und sogar OVER (PARTITION BY) funktioniert:

SELECT *,
    [dbo].[Product](T.VALUE) 
    OVER (PARTITION BY PAR_COLUMN) AS P
FROM @TEST AS T;

Aber das Ausführen des Produkts mit OVER (PARTITION BY ... ORDER BY ...) funktioniert nicht (überprüft mit SQL Server 2014 Express 12.0.2000.8):

SELECT *,
    [dbo].[Product](T.VALUE) 
    OVER (PARTITION BY T.PAR_COLUMN ORDER BY T.PERIOD 
          ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS CUM_MUL
FROM @TEST AS T;

Eine Suche hat dieses Verbindungselement gefunden , die als "Won't Fix" geschlossen wird und diese Frage .

Der C#-Code:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.IO;
using System.Collections.Generic;
using System.Text;

namespace RunningProduct
{
    [Serializable]
    [SqlUserDefinedAggregate(
        Format.UserDefined,
        MaxByteSize = 17,
        IsInvariantToNulls = true,
        IsInvariantToDuplicates = false,
        IsInvariantToOrder = true,
        IsNullIfEmpty = true)]
    public struct Product : IBinarySerialize
    {
        private bool m_bIsNull; // 1 byte storage
        private decimal m_Product; // 16 bytes storage

        public void Init()
        {
            this.m_bIsNull = true;
            this.m_Product = 1;
        }

        public void Accumulate(
            [SqlFacet(Precision = 22, Scale = 6)] SqlDecimal ParamValue)
        {
            if (ParamValue.IsNull) return;

            this.m_bIsNull = false;
            this.m_Product *= ParamValue.Value;
        }

        public void Merge(Product other)
        {
            SqlDecimal otherValue = other.Terminate();
            this.Accumulate(otherValue);
        }

        [return: SqlFacet(Precision = 22, Scale = 6)]
        public SqlDecimal Terminate()
        {
            if (m_bIsNull)
            {
                return SqlDecimal.Null;
            }
            else
            {
                return m_Product;
            }
        }

        public void Read(BinaryReader r)
        {
            this.m_bIsNull = r.ReadBoolean();
            this.m_Product = r.ReadDecimal();
        }

        public void Write(BinaryWriter w)
        {
            w.Write(this.m_bIsNull);
            w.Write(this.m_Product);
        }
    }
}

CLR-Assembly installieren:

-- Turn advanced options on
EXEC sys.sp_configure @configname = 'show advanced options', @configvalue = 1 ;
GO
RECONFIGURE WITH OVERRIDE ;
GO
-- Enable CLR
EXEC sys.sp_configure @configname = 'clr enabled', @configvalue = 1 ;
GO
RECONFIGURE WITH OVERRIDE ;
GO

CREATE ASSEMBLY [RunningProduct]
AUTHORIZATION [dbo]
FROM 'C:\RunningProduct\RunningProduct.dll'
WITH PERMISSION_SET = SAFE;
GO

CREATE AGGREGATE [dbo].[Product](@ParamValue numeric(22,6))
RETURNS numeric(22,6)
EXTERNAL NAME [RunningProduct].[RunningProduct.Product];
GO

Diese Frage diskutiert die Berechnung einer laufenden SUM in großen Details und Paul White zeigt es in seiner Antwort wie man eine CLR-Funktion schreibt, die die laufende SUM effizient berechnet. Es wäre ein guter Anfang, um eine Funktion zu schreiben, die das laufende Produkt berechnet.

Beachten Sie, dass er einen anderen Ansatz verwendet. Anstatt ein benutzerdefiniertes Aggregat zu erstellen Funktion erstellt Paul eine Funktion, die eine Tabelle zurückgibt. Die Funktion liest die Originaldaten in den Speicher und führt alle erforderlichen Berechnungen durch.

Es kann einfacher sein, den gewünschten Effekt zu erzielen, indem Sie diese Berechnungen auf Ihrer Client-Seite mit der Programmiersprache Ihrer Wahl implementieren. Lesen Sie einfach die gesamte Tabelle und berechnen Sie das laufende Produkt auf dem Client. Das Erstellen einer CLR-Funktion ist sinnvoll, wenn das auf dem Server berechnete laufende Produkt ein Zwischenschritt in komplexeren Berechnungen ist, die Daten weiter aggregieren würden.

Eine weitere Idee, die mir in den Sinn kommt.

Finden Sie eine mathematische .NET-Bibliothek eines Drittanbieters, die Log anbietet und Exp arbeitet mit hoher Präzision. Erstellen Sie eine CLR-Version dieser Skalare Funktionen. Und dann verwenden Sie EXP + LOG + SUM() Over (Order by) Ansatz, wobei SUM ist die integrierte T-SQL-Funktion, die Over (Order by) unterstützt und Exp und Log sind benutzerdefinierte CLR-Funktionen, die nicht float zurückgeben , aber hochpräzise decimal .

Beachten Sie, dass Berechnungen mit hoher Genauigkeit auch langsam sein können. Und die Verwendung von CLR-Skalarfunktionen in der Abfrage kann sie ebenfalls verlangsamen.