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.