W czystym T-SQL LOG
i EXP
działają z float
typ (8 bajtów), który ma tylko 15-17 cyfr znaczących
. Nawet ta ostatnia piętnasta cyfra może stać się niedokładna, jeśli zsumujesz wystarczająco duże wartości. Twoje dane to numeric(22,6)
, więc 15 cyfr znaczących to za mało.
POWER
może zwrócić numeric
wpisz z potencjalnie większą precyzją, ale jest to dla nas mało przydatne, ponieważ oba LOG
i LOG10
może zwrócić tylko float
w każdym razie.
Aby zademonstrować problem, zmienię typ w twoim przykładzie na numeric(15,0)
i użyj POWER
zamiast 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;
Wynik
+------------+--------+-------+-----------------+
| 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 |
+------------+--------+-------+-----------------+
Każdy krok tutaj traci precyzję. Obliczanie LOG traci precyzję, SUM traci precyzję, EXP/POWER traci precyzję. Dzięki tym wbudowanym funkcjom nie sądzę, że możesz wiele z tym zrobić.
Odpowiedź brzmi - użyj CLR z C# decimal
wpisz (nie double
), który obsługuje większą precyzję (28-29 cyfr znaczących). Twój oryginalny typ SQL numeric(22,6)
pasowałby do niego. I nie potrzebowałbyś sztuczki z LOG/EXP
.
Ups. Próbowałem utworzyć agregat CLR, który oblicza produkt. Działa w moich testach, ale tylko jako prosty agregat, tj.
To działa:
SELECT T.PAR_COLUMN, [dbo].[Product](T.VALUE) AS P
FROM @TEST AS T
GROUP BY T.PAR_COLUMN;
A nawet OVER (PARTITION BY)
działa:
SELECT *,
[dbo].[Product](T.VALUE)
OVER (PARTITION BY PAR_COLUMN) AS P
FROM @TEST AS T;
Ale uruchamianie produktu przy użyciu OVER (PARTITION BY ... ORDER BY ...)
nie działa (sprawdzone w 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;
W wyniku wyszukiwania znaleziono ten element połączenia , który jest zamknięty jako „Nie naprawi”, a ten pytanie .
Kod 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);
}
}
}
Zainstaluj zespół 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
To pytanie szczegółowo omawia obliczanie bieżącej sumy i Paul White pokazuje w swojej odpowiedzi jak napisać funkcję CLR, która efektywnie oblicza działanie SUMA. Byłby to dobry początek do napisania funkcji, która oblicza działający produkt.
Zauważ, że stosuje inne podejście. Zamiast tworzyć niestandardowe agregaty funkcji, Paweł tworzy funkcję, która zwraca tabelę. Funkcja wczytuje oryginalne dane do pamięci i wykonuje wszystkie wymagane obliczenia.
Osiągnięcie pożądanego efektu może być łatwiejsze, jeśli wykonasz te obliczenia po stronie klienta przy użyciu wybranego przez Ciebie języka programowania. Wystarczy przeczytać całą tabelę i obliczyć działający produkt u klienta. Tworzenie funkcji CLR ma sens, jeśli działający produkt obliczany na serwerze jest etapem pośrednim w bardziej złożonych obliczeniach, które dalej agregują dane.
Jeszcze jeden pomysł, który przychodzi mi do głowy.
Znajdź bibliotekę matematyczną .NET innej firmy, która oferuje Log
i Exp
funkcje z wysoką precyzją. Utwórz wersję CLR tych skalarnych Funkcje. A następnie użyj EXP + LOG + SUM() Over (Order by)
podejście, gdzie SUM
jest wbudowaną funkcją T-SQL, która obsługuje Over (Order by)
i Exp
i Log
to niestandardowe funkcje CLR, które nie zwracają float
, ale bardzo precyzyjny decimal
.
Pamiętaj, że obliczenia o wysokiej precyzji mogą być również powolne. A użycie funkcji skalarnych CLR w zapytaniu może również spowolnić je.