sql >> Databasteknik >  >> RDS >> Sqlserver

Avrundningsproblem i LOG- och EXP-funktioner

I ren T-SQL LOG och EXP arbeta med float typ (8 byte), som bara har 15–17 signifikanta siffror . Även den sista 15:e siffran kan bli felaktig om du summerar tillräckligt stora värden. Dina data är numeric(22,6) , så 15 signifikanta siffror räcker inte.

POWER kan returnera numeric typ med potentiellt högre precision, men det är till liten nytta för oss, eftersom båda LOG och LOG10 kan endast returnera float i alla fall.

För att demonstrera problemet kommer jag att ändra typen i ditt exempel till numeric(15,0) och använd POWER istället för 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;

Resultat

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

Varje steg här tappar precisionen. Att beräkna LOG tappar precision, SUM tappar precision, EXP/POWER tappar precision. Med dessa inbyggda funktioner tror jag inte att du kan göra så mycket åt det.

Så svaret är - använd CLR med C# decimal typ (inte double ), som stöder högre precision (28-29 signifikanta siffror). Din ursprungliga SQL-typ numeric(22,6) skulle passa in i det. Och du skulle inte behöva tricket med LOG/EXP .

Hoppsan. Jag försökte göra ett CLR-aggregat som beräknar produkt. Det fungerar i mina tester, men bara som ett enkelt aggregat, d.v.s.

Detta fungerar:

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

Och till och med OVER (PARTITION BY) fungerar:

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

Men kör produkt med OVER (PARTITION BY ... ORDER BY ...) fungerar inte (kontrollerat med 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;

En sökning hittade detta anslutningsobjekt , som är stängd som "Won't Fix" och detta fråga .

C#-koden:

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);
        }
    }
}

Installera CLR-enhet:

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

Den här frågan diskuterar beräkningen av en löpande SUMMA i detaljer och Paul White visar i sitt svar hur man skriver en CLR-funktion som beräknar löpande SUM effektivt. Det skulle vara en bra början för att skriva en funktion som beräknar löpande produkt.

Observera att han använder ett annat tillvägagångssätt. Istället för att göra ett anpassat aggregat funktion, gör Paul en funktion som returnerar en tabell. Funktionen läser in originaldata i minnet och utför alla nödvändiga beräkningar.

Det kan vara lättare att uppnå önskad effekt genom att implementera dessa beräkningar på din klientsida med hjälp av det programmeringsspråk du väljer. Läs bara hela tabellen och beräkna löpande produkt på klienten. Att skapa CLR-funktion är vettigt om den körande produkten som beräknas på servern är ett mellansteg i en mer komplex beräkning som skulle aggregera data ytterligare.

Ännu en idé som dyker upp.

Hitta ett tredjeparts .NET matematikbibliotek som erbjuder Log och Exp fungerar med hög precision. Gör en CLR-version av dessa skalärer funktioner. Och använd sedan EXP + LOG + SUM() Over (Order by) tillvägagångssätt, där SUM är den inbyggda T-SQL-funktionen, som stöder Over (Order by) och Exp och Log är anpassade CLR-funktioner som inte returnerar float , men decimal med hög precision .

Observera att beräkningar med hög precision också kan vara långsamma. Och att använda CLR-skalära funktioner i frågan kan också göra det långsamt.



  1. Lagring av dynamisk formulärdata i DBMS, letar efter det optimala tillvägagångssättet

  2. Simulerande fördröjningsfunktion på MySQL

  3. Kan frågor som läser tabellvariabler generera parallella exponeringsplaner i SQL Server 2008?

  4. Kan Django-modeller använda MySQL-funktioner?