Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Problema di arrotondamento nelle funzioni LOG ed EXP

In puro LOG T-SQL e EXP operare con il float tipo (8 byte), che ha solo 15-17 cifre significative . Anche l'ultima quindicesima cifra può diventare imprecisa se si sommano valori sufficientemente grandi. I tuoi dati sono numeric(22,6) , quindi 15 cifre significative non sono sufficienti.

POWER può restituire numeric digitare con una precisione potenzialmente maggiore, ma per noi è di scarsa utilità, perché sia ​​LOG e LOG10 può restituire solo float comunque.

Per dimostrare il problema, cambierò il tipo nel tuo esempio in numeric(15,0) e usa POWER invece di 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;

Risultato

+------------+--------+-------+-----------------+
| 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 |
+------------+--------+-------+-----------------+

Ogni passo qui perde precisione. Il calcolo di LOG perde precisione, SUM perde precisione, EXP/POWER perde precisione. Con queste funzioni integrate non credo che tu possa farci molto.

Quindi, la risposta è:usa CLR con C# decimal digitare (non double ), che supporta una maggiore precisione (28-29 cifre significative). Il tuo tipo SQL originale numeric(22,6) ci si adatterebbe. E non avresti bisogno del trucco con LOG/EXP .

Ops. Ho provato a creare un aggregato CLR che calcola Product. Funziona nei miei test, ma solo come un semplice aggregato, cioè

Funziona:

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

E anche OVER (PARTITION BY) funziona:

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

Ma, eseguendo il prodotto usando OVER (PARTITION BY ... ORDER BY ...) non funziona (verificato con 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;

Una ricerca ha trovato questo elemento di connessione , che viene chiuso come "Non risolverà" e questo domanda .

Il codice C#:

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);
        }
    }
}

Installa l'assieme CLR:

-- 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

Questa domanda discute il calcolo di una SOMMA corrente in dettaglio e Paul White mostra nella sua risposta come scrivere una funzione CLR che calcoli l'esecuzione di SUM in modo efficiente. Sarebbe un buon inizio per scrivere una funzione che calcola l'esecuzione di Product.

Nota che usa un approccio diverso. Invece di creare un aggregato personalizzato funzione, Paul crea una funzione che restituisce una tabella. La funzione legge i dati originali in memoria ed esegue tutti i calcoli richiesti.

Potrebbe essere più facile ottenere l'effetto desiderato implementando questi calcoli sul lato client utilizzando il linguaggio di programmazione di tua scelta. Basta leggere l'intera tabella e calcolare il prodotto in esecuzione sul client. La creazione della funzione CLR ha senso se il prodotto in esecuzione calcolato sul server è un passaggio intermedio in calcoli più complessi che aggregherebbero ulteriormente i dati.

Un'altra idea che mi viene in mente.

Trova una libreria matematica .NET di terze parti che offra Log e Exp funzioni con alta precisione. Crea una versione CLR di questi scalari funzioni. E poi usa EXP + LOG + SUM() Over (Order by) approccio, dove SUM è la funzione T-SQL integrata, che supporta Over (Order by) e Exp e Log sono funzioni CLR personalizzate che restituiscono non float , ma decimal ad alta precisione .

Si noti che anche i calcoli ad alta precisione possono essere lenti. Inoltre, l'utilizzo delle funzioni scalari CLR nella query potrebbe rallentarlo.