sql >> Databáze >  >> RDS >> Sqlserver

Problém se zaokrouhlováním ve funkcích LOG a EXP

V čistém T-SQL LOG a EXP pracovat s float typ (8 bajtů), který má pouze 15–17 platných číslic . I tato poslední 15. číslice se může stát nepřesnou, pokud sečtete dostatečně velké hodnoty. Vaše data jsou numeric(22,6) , takže 15 platných číslic nestačí.

POWER může vrátit numeric typu s potenciálně vyšší přesností, ale je to pro nás málo užitečné, protože oba LOG a LOG10 může vrátit pouze float každopádně.

Abych demonstroval problém, změním typ ve vašem příkladu na numeric(15,0) a použijte POWER místo 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;

Výsledek

+------------+--------+-------+-----------------+
| 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ždý krok zde ztrácí přesnost. Výpočet LOG ztratí přesnost, SUM ztratí přesnost, EXP/POWER ztratí přesnost. S těmito vestavěnými funkcemi s tím podle mě moc nenaděláte.

Takže odpověď zní - použijte CLR s C# decimal typ (nikoli double ), který podporuje vyšší přesnost (28–29 platných číslic). Váš původní typ SQL numeric(22,6) by se do toho hodil. A nebudete potřebovat trik s LOG/EXP .

Jejda. Pokusil jsem se vytvořit agregát CLR, který počítá produkt. V mých testech funguje, ale pouze jako jednoduchý agregát, tj.

Toto funguje:

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

A dokonce OVER (PARTITION BY) funguje:

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

Ale spouštění produktu pomocí OVER (PARTITION BY ... ORDER BY ...) nefunguje (zkontrolováno pomocí 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;

Při hledání byla nalezena tato položka připojení , který je uzavřen jako „Won't Fix“ a toto otázka .

Kód 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);
        }
    }
}

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

Tato otázka podrobně popisuje výpočet průběžného SUM a Paul White ukazuje ve své odpovědi jak napsat funkci CLR, která vypočítá efektivní běh SUM. Byl by to dobrý začátek pro psaní funkce, která vypočítá běžící produkt.

Všimněte si, že používá jiný přístup. Místo vytvoření vlastního agregátu Paul vytvoří funkci, která vrací tabulku. Funkce načte původní data do paměti a provede všechny požadované výpočty.

Může být snazší dosáhnout požadovaného efektu implementací těchto výpočtů na straně klienta pomocí programovacího jazyka dle vašeho výběru. Stačí si přečíst celou tabulku a vypočítat běžící produkt na klientovi. Vytvoření funkce CLR má smysl, pokud běžící produkt vypočítaný na serveru je přechodným krokem ve složitějších výpočtech, které by dále agregovaly data.

Ještě jeden nápad, který mě napadá.

Najděte matematickou knihovnu .NET třetí strany, která nabízí Log a Exp funkce s vysokou přesností. Vytvořte verzi CLR těchto skalárních funkcí. A pak použijte EXP + LOG + SUM() Over (Order by) přístup, kde SUM je vestavěná funkce T-SQL, která podporuje Over (Order by) a Exp a Log jsou vlastní funkce CLR, které nevracejí float , ale s vysokou přesností decimal .

Pamatujte, že výpočty s vysokou přesností mohou být také pomalé. A použití skalárních funkcí CLR v dotazu může také zpomalit.



  1. Omezení cizího klíče polymorfní asociace. Je to dobré řešení?

  2. Jaký je rozdíl mezi účty Oracle SYS a SYSTEM?

  3. MySQL a JDBC s rewriteBatchedStatements=true

  4. pdo lastInsertId vrací nulu (0)