Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

Problem zaokrąglania w funkcjach LOG i EXP

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.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak przekształcić wiersze w kolumny w serwerze sql 2005?

  2. Rzędy utrzymujące maksimum grupowe określonej kolumny (jak zabijać duplikaty...)

  3. Warunki wyścigu kolejki procesów SQL Server

  4. Obliczanie czasu trwania SQL

  5. SQL Server - oblicz czas, jaki upłynął między dwoma znacznikami daty i godziny w formacie GG:MM:SS