Sqlserver
 sql >> база данни >  >> RDS >> Sqlserver

Проблем със закръгляването във функциите LOG и EXP

В чист T-SQL LOG и EXP работете с float тип (8 байта), който има само 15-17 значещи цифри . Дори последната 15-та цифра може да стане неточна, ако сумирате достатъчно големи стойности. Вашите данни са numeric(22,6) , така че 15 значещи цифри не са достатъчни.

POWER може да върне numeric тип с потенциално по-висока точност, но е от малка полза за нас, защото и двете LOG и LOG10 може да върне само float както и да е.

За да демонстрирам проблема, ще променя типа във вашия пример на numeric(15,0) и използвайте POWER вместо 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;

Резултат

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

Всяка стъпка тук губи прецизност. Изчисляването на LOG губи точност, SUM губи точност, EXP/POWER губи точност. С тези вградени функции не мисля, че можете да направите много по въпроса.

И така, отговорът е - използвайте CLR с C# decimal тип (не double ), който поддържа по-висока точност (28-29 значещи цифри). Вашият оригинален SQL тип numeric(22,6) би се вписал в него. И няма да имате нужда от трика с LOG/EXP .

опа Опитах се да направя CLR агрегат, който изчислява Product. Работи в моите тестове, но само като обикновен агрегат, т.е.

Това работи:

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

И дори OVER (PARTITION BY) работи:

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

Но стартиране на продукт чрез OVER (PARTITION BY ... ORDER BY ...) не работи (проверено с 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;

Търсене намери този елемент за свързване , което е затворено като „Няма да се коригира“ и това въпрос .

Кодът на 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);
        }
    }
}

Инсталирайте 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

Този въпрос обсъжда изчисляването на текуща SUM в големи подробности и Пол Уайт показва в своя отговор как да напиша CLR функция, която изчислява ефективно SUM. Би било добро начало за писане на функция, която изчислява работещ продукт.

Имайте предвид, че той използва различен подход. Вместо да правите персонализиранагрегат функция, Пол прави функция, която връща таблица. Функцията чете оригиналните данни в паметта и извършва всички необходими изчисления.

Може да е по-лесно да постигнете желания ефект, като приложите тези изчисления от страна на вашия клиент, като използвате езика за програмиране по ваш избор. Просто прочетете цялата таблица и изчислете текущия продукт на клиента. Създаването на CLR функция има смисъл, ако работещият продукт, изчислен на сървъра, е междинна стъпка в по-сложни изчисления, които допълнително ще агрегират данни.

Още една идея, която ми идва на ум.

Намерете .NET математическа библиотека на трета страна, която предлага Log и Exp функционира с висока точност. Направете CLR версия на тези скалари функции. След това използвайте EXP + LOG + SUM() Over (Order by) подход, където SUM е вградената T-SQL функция, която поддържа Over (Order by) и Exp и Log са персонализирани CLR функции, които връщат не float , но decimal с висока точност .

Имайте предвид, че изчисленията с висока точност също могат да бъдат бавни. И използването на CLR скаларни функции в заявката също може да я забави.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да коригирате конфликт на съпоставяне в заявка на SQL Server?

  2. SQL, въпроси относно присъединяването

  3. Проактивни проверки на състоянието на SQL Server, част 4:ERRORLOG

  4. Моята заявка Select SUM връща нула. Трябва да върне 0

  5. SQL Server - Закръглете стойностите на TIME до следващата минута