Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Problema de arredondamento nas funções LOG e EXP


Em T-SQL puro LOG e EXP operar com o float tipo (8 bytes), que tem apenas 15-17 dígitos significativos . Mesmo o último 15º dígito pode se tornar impreciso se você somar valores grandes o suficiente. Seus dados são numeric(22,6) , portanto, 15 dígitos significativos não são suficientes.

POWER pode retornar numeric tipo com precisão potencialmente maior, mas é de pouca utilidade para nós, porque tanto LOG e LOG10 pode retornar apenas float de qualquer forma.

Para demonstrar o problema, alterarei o tipo no seu exemplo para numeric(15,0) e use POWER em vez 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 passo aqui perde precisão. Calcular LOG perde precisão, SUM perde precisão, EXP/POWER perde precisão. Com essas funções internas, não acho que você possa fazer muito sobre isso.

Portanto, a resposta é - use CLR com C# decimal tipo (não double ), que suporta maior precisão (28-29 dígitos significativos). Seu tipo de SQL original numeric(22,6) caberia nele. E você não precisaria do truque com LOG/EXP .

Ops. Tentei fazer um agregado CLR que calcula o Produto. Funciona nos meus testes, mas apenas como um agregado simples, ou seja,

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

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

Mas, executando o produto usando OVER (PARTITION BY ... ORDER BY ...) não funciona (verificado com 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;

Uma pesquisa encontrou este item de conexão , que está fechado como "Não será corrigido" e isso pergunta .

O 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 o conjunto 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 pergunta discute o cálculo de um SUM em execução em detalhes e Paul White mostra em sua resposta como escrever uma função CLR que calcula a execução de SUM de forma eficiente. Seria um bom começo para escrever uma função que calcula o produto em execução.

Observe que ele usa uma abordagem diferente. Em vez de criar um agregado personalizado função, Paul faz uma função que retorna uma tabela. A função lê os dados originais na memória e executa todos os cálculos necessários.

Pode ser mais fácil obter o efeito desejado implementando esses cálculos no lado do cliente usando a linguagem de programação de sua escolha. Basta ler toda a tabela e calcular o produto em execução no cliente. A criação da função CLR faz sentido se o produto em execução calculado no servidor for uma etapa intermediária em cálculos mais complexos que agregariam dados ainda mais.

Mais uma ideia que me vem à cabeça.

Encontre uma biblioteca matemática .NET de terceiros que ofereça Log e Exp funções com alta precisão. Faça uma versão CLR desses escalares funções. E então use o EXP + LOG + SUM() Over (Order by) abordagem, onde SUM é a função T-SQL integrada, que suporta Over (Order by) e Exp e Log são funções CLR personalizadas que não retornam float , mas decimal de alta precisão .

Observe que os cálculos de alta precisão também podem ser lentos. E o uso de funções escalares CLR na consulta também pode torná-la lenta.