In pure T-SQL LOG
en EXP
bedien met de float
type (8 bytes), dat alleen 15-17 significante cijfers heeft
. Zelfs dat laatste 15e cijfer kan onnauwkeurig worden als u waarden optelt die groot genoeg zijn. Uw gegevens zijn numeric(22,6)
, dus 15 significante cijfers is niet genoeg.
POWER
kan numeric
retourneren typ met potentieel hogere precisie, maar het heeft voor ons weinig zin, omdat zowel LOG
en LOG10
kan alleen float
return retourneren hoe dan ook.
Om het probleem te demonstreren, verander ik het type in uw voorbeeld in numeric(15,0)
en gebruik POWER
in plaats van 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;
Resultaat
+------------+--------+-------+-----------------+
| 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 |
+------------+--------+-------+-----------------+
Elke stap hier verliest precisie. Het berekenen van LOG verliest precisie, SUM verliest precisie, EXP/POWER verliest precisie. Met deze ingebouwde functies denk ik niet dat je er veel aan kunt doen.
Het antwoord is dus - gebruik CLR met C# decimal
type (niet double
), die een hogere precisie ondersteunt (28-29 significante cijfers). Uw originele SQL-type numeric(22,6)
zou erin passen. En je zou de truc met LOG/EXP
. niet nodig hebben .
Oeps. Ik heb geprobeerd een CLR-aggregaat te maken dat Product berekent. Het werkt in mijn tests, maar alleen als een eenvoudig aggregaat, d.w.z.
Dit werkt:
SELECT T.PAR_COLUMN, [dbo].[Product](T.VALUE) AS P
FROM @TEST AS T
GROUP BY T.PAR_COLUMN;
En zelfs OVER (PARTITION BY)
werkt:
SELECT *,
[dbo].[Product](T.VALUE)
OVER (PARTITION BY PAR_COLUMN) AS P
FROM @TEST AS T;
Maar, product uitvoeren met OVER (PARTITION BY ... ORDER BY ...)
werkt niet (gecontroleerd met 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;
Een zoekopdracht vond dit verbind item , die is gesloten als "Won't Fix" en deze vraag .
De C#-code:
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);
}
}
}
CLR-assemblage installeren:
-- 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
Deze vraag bespreekt de berekening van een lopende SOM in detail en Paul White laat zien in zijn antwoord hoe een CLR-functie te schrijven die de lopende SUM efficiënt berekent. Het zou een goed begin zijn voor het schrijven van een functie die het draaiende Product berekent.
Merk op dat hij een andere benadering gebruikt. In plaats van een aangepast aggregaat te maken functie, maakt Paul een functie die een tabel retourneert. De functie leest de originele gegevens in het geheugen en voert alle vereiste berekeningen uit.
Het kan gemakkelijker zijn om het gewenste effect te bereiken door deze berekeningen aan uw clientzijde uit te voeren met behulp van de programmeertaal van uw keuze. Lees gewoon de hele tabel en bereken het lopende product op de klant. Het creëren van een CLR-functie is zinvol als het actieve product dat op de server wordt berekend, een tussenstap is in een complexere berekening die gegevens verder zou samenvoegen.
Nog een idee dat in me opkomt.
Zoek een .NET-wiskundebibliotheek van derden die Log
. biedt en Exp
werkt met hoge precisie. Maak een CLR-versie van deze scalar functies. En gebruik dan de EXP + LOG + SUM() Over (Order by)
benadering, waarbij SUM
is de ingebouwde T-SQL-functie, die Over (Order by)
ondersteunt en Exp
en Log
zijn aangepaste CLR-functies die niet float
return retourneren , maar met hoge precisie decimal
.
Houd er rekening mee dat berekeningen met hoge precisie ook traag kunnen zijn. En het gebruik van CLR scalaire functies in de query kan het ook traag maken.