Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Problème d'arrondi dans les fonctions LOG et EXP

En pur T-SQL LOG et EXP fonctionner avec le float type (8 octets), qui n'a que 15-17 chiffres significatifs . Même ce 15e dernier chiffre peut devenir inexact si vous additionnez des valeurs suffisamment grandes. Vos données sont numeric(22,6) , donc 15 chiffres significatifs ne suffisent pas.

POWER peut renvoyer numeric type avec une précision potentiellement plus élevée, mais cela nous est peu utile, car les deux LOG et LOG10 ne peut renvoyer que float de toute façon.

Pour illustrer le problème, je vais changer le type dans votre exemple en numeric(15,0) et utilisez POWER au lieu de 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;

Résultat

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

Chaque pas ici perd en précision. Le calcul de LOG perd en précision, SUM perd en précision, EXP/POWER perd en précision. Avec ces fonctions intégrées, je ne pense pas que vous puissiez y faire grand-chose.

Donc, la réponse est - utilisez CLR avec C# decimal type (et non double ), qui prend en charge une plus grande précision (28-29 chiffres significatifs). Votre type SQL d'origine numeric(22,6) s'y intégrerait. Et vous n'auriez pas besoin de l'astuce avec LOG/EXP .

Oops. J'ai essayé de créer un agrégat CLR qui calcule Product. Cela fonctionne dans mes tests, mais seulement comme un simple agrégat, c'est-à-dire

Cela fonctionne :

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

Et même OVER (PARTITION BY) fonctionne :

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

Mais, exécuter le produit en utilisant OVER (PARTITION BY ... ORDER BY ...) ne fonctionne pas (vérifié avec 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;

Une recherche a trouvé cet élément de connexion , qui est fermé comme "Won't Fix" et cela question .

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

Installez l'assemblage 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

Cette question discute du calcul d'une somme en cours d'exécution dans les moindres détails et Paul White montre dans sa réponse comment écrire une fonction CLR qui calcule efficacement l'exécution de SUM. Ce serait un bon début pour écrire une fonction qui calcule le produit en cours d'exécution.

Notez qu'il utilise une approche différente. Au lieu de créer un agrégat personnalisé fonction, Paul crée une fonction qui renvoie une table. La fonction lit les données d'origine dans la mémoire et effectue tous les calculs requis.

Il peut être plus facile d'obtenir l'effet souhaité en implémentant ces calculs côté client en utilisant le langage de programmation de votre choix. Il suffit de lire tout le tableau et de calculer le produit en cours d'exécution sur le client. La création de la fonction CLR a du sens si le produit en cours calculé sur le serveur est une étape intermédiaire dans des calculs plus complexes qui agrégeraient davantage les données.

Encore une idée qui me vient à l'esprit.

Trouvez une bibliothèque mathématique .NET tierce qui offre Log et Exp fonctionne avec une grande précision. Créez une version CLR de ces scalaire les fonctions. Et puis utilisez le EXP + LOG + SUM() Over (Order by) approche, où SUM est la fonction T-SQL intégrée, qui prend en charge Over (Order by) et Exp et Log sont des fonctions CLR personnalisées qui ne renvoient pas float , mais decimal de haute précision .

Notez que les calculs de haute précision peuvent également être lents. Et l'utilisation de fonctions scalaires CLR dans la requête peut également la ralentir.