sql >> Base de Datos >  >> RDS >> Sqlserver

Problema de redondeo en las funciones LOG y EXP

En T-SQL puro LOG y EXP operar con el float tipo (8 bytes), que tiene solo 15-17 dígitos significativos . Incluso ese último dígito 15 puede volverse inexacto si suma valores lo suficientemente grandes. Tus datos son numeric(22,6) , por lo que 15 dígitos significativos no son suficientes.

POWER puede devolver numeric escribir con una precisión potencialmente mayor, pero es de poca utilidad para nosotros, porque tanto LOG y LOG10 solo puede devolver float de todos modos.

Para demostrar el problema, cambiaré el tipo en su ejemplo a numeric(15,0) y usa POWER en lugar 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;

Resultado

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

Cada paso aquí pierde precisión. Calcular LOG pierde precisión, SUM pierde precisión, EXP/POWER pierde precisión. Con estas funciones integradas, no creo que puedas hacer mucho al respecto.

Entonces, la respuesta es:use CLR con C# decimal tipo (no double ), que admite una mayor precisión (28-29 dígitos significativos). Tu tipo de SQL original numeric(22,6) encajaría en él. Y no necesitarías el truco con LOG/EXP .

Ups. Traté de hacer un agregado CLR que calcule el Producto. Funciona en mis pruebas, pero solo como un agregado simple, es decir,

Esto funciona:

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

E incluso OVER (PARTITION BY) funciona:

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

Pero, ejecutar el producto usando OVER (PARTITION BY ... ORDER BY ...) no funciona (comprobado 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 búsqueda encontró este elemento de conexión , que está cerrado como "No arreglará" y esto pregunta .

El código 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);
        }
    }
}

Instale el ensamblaje 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

Esta pregunta analiza el cálculo de una SUM en ejecución con gran detalle y Paul White muestra en su respuesta cómo escribir una función CLR que calcule ejecutar SUM de manera eficiente. Sería un buen comienzo para escribir una función que calcule el Producto en ejecución.

Tenga en cuenta que utiliza un enfoque diferente. En lugar de crear un agregado personalizado función, Paul crea una función que devuelve una tabla. La función lee los datos originales en la memoria y realiza todos los cálculos necesarios.

Puede ser más fácil lograr el efecto deseado implementando estos cálculos en su lado del cliente utilizando el lenguaje de programación de su elección. Simplemente lea toda la tabla y calcule el producto corriente en el cliente. La creación de la función CLR tiene sentido si el producto en ejecución calculado en el servidor es un paso intermedio en cálculos más complejos que agregarían más datos.

Una idea más que se me ocurre.

Encuentre una biblioteca matemática de .NET de terceros que ofrezca Log y Exp funciones con alta precisión. Haz una versión CLR de estos escalares funciones Y luego use EXP + LOG + SUM() Over (Order by) enfoque, donde SUM es la función T-SQL incorporada, que admite Over (Order by) y Exp y Log son funciones CLR personalizadas que devuelven no float , pero decimal de alta precisión .

Tenga en cuenta que los cálculos de alta precisión también pueden ser lentos. Y el uso de funciones escalares CLR en la consulta también puede hacer que sea más lento.