【问题标题】:Rounding issue in LOG and EXP functionsLOG 和 EXP 函数中的舍入问题
【发布时间】:2016-02-29 13:32:27
【问题描述】:

我正在尝试执行累积乘法。我正在尝试两种方法来做到这一点

样本数据:

DECLARE @TEST TABLE
  (
     PAR_COLUMN INT,
     PERIOD     INT,
     VALUE      NUMERIC(22, 6)
  ) 
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)

注意:value 列中的数据永远不会是整数,值会有小数部分。为了显示近似问题,我将示例值保留为整数。


方法一:EXP + LOG + SUM() Over(Order by)

在这种方法中,我使用EXP + LOG + SUM() Over(Order by) 技术来查找累积乘法。在这种方法中,数值不准确;结果中存在一些舍入和近似问题。

SELECT *,
       Exp(Sum(Log(Abs(NULLIF(VALUE, 0))))
             OVER(
               PARTITION BY PAR_COLUMN
               ORDER BY PERIOD)) AS CUM_MUL
FROM   @TEST;

结果:

PAR_COLUMN  PERIOD  VALUE       CUM_MUL
----------  ------  ---------   ----------------
1           601     10.000000   10
1           602     20.000000   200             -- 10 * 20 = 200(correct)
1           603     30.000000   6000.00000000001 -- 200 * 30 = 6000.000000000 (not 6000.00000000001) incorrect
1           604     40.000000   240000
1           605     50.000000   12000000
1           606     60.000000   720000000.000001  -- 12000000 * 60 = 720000000.000000 (not 720000000.000001) incorrect
2           601     100.000000  100
2           602     200.000000  20000
2           603     300.000000  5999999.99999999 -- 20000.000000 *300.000000 = 6000000.000000 (not 5999999.99999999) incorrect
2           604     400.000000  2399999999.99999  
2           605     500.000000  1199999999999.99
2           606     600.000000  719999999999998

方法2:传统乘法(递归CTE)

此方法完美运行,没有任何舍入或近似问题。

;WITH CTE
     AS (SELECT TOP 1 WITH TIES PAR_COLUMN,
                                PERIOD,
                                VALUE,
                                CUM_MUL = VALUE
         FROM   @TEST
         ORDER  BY PERIOD
         UNION ALL
         SELECT T.PAR_COLUMN,
                T.PERIOD,
                T.VALUE,
                Cast(T.VALUE * C.CUM_MUL AS NUMERIC(22, 6))
         FROM   CTE C
                INNER JOIN @TEST T
                        ON C.PAR_COLUMN = T.PAR_COLUMN
                           AND T.PERIOD = C.PERIOD + 1)
SELECT *
FROM   CTE 
ORDER BY PAR_COLUMN,PERIOD

结果

PAR_COLUMN  PERIOD  VALUE       CUM_MUL
----------  ------  ---------   ----------------
1           601     10.000000   10.000000
1           602     20.000000   200.000000
1           603     30.000000   6000.000000
1           604     40.000000   240000.000000
1           605     50.000000   12000000.000000
1           606     60.000000   720000000.000000
2           601     100.000000  100.000000
2           602     200.000000  20000.000000
2           603     300.000000  6000000.000000
2           604     400.000000  2400000000.000000
2           605     500.000000  1200000000000.000000
2           606     600.000000  720000000000000.000000

谁能告诉我为什么方法 1 中的值不准确以及如何解决?我尝试将数据类型更改为Float,并在numeric 中增加scale,但没有用。

我很想用方法一,比方法二快很多。

编辑:现在我知道了近似的原因。任何人都可以找到解决此问题的方法吗?

【问题讨论】:

  • 原因很简单。精度是有限的,所以SELECT EXP (LOG (1000)) 不会返回 1000
  • 是否有任何解决方法
  • @lad2025 - 实际上它返回1000。但是EXP (LOG (99787.564)) 返回99787.5639999999
  • 我会说这取决于检查demo
  • 您有什么理由不使用 CLR 聚合?喜欢这里stackoverflow.com/a/30757189/73226

标签: sql sql-server sql-server-2008 sql-server-2012


【解决方案1】:

LOG()EXP() 将参数隐式转换为 float 数据类型,它们是近似值。

【讨论】:

  • @MM93 不,不是真的。 float 基本上是一个 IEEE 754 实现。它只会精确到 6 或 7 位数字。如果你四舍五入到那个精度,你会得到一个更准确的结果,但这有点难以实现,但是,因为ROUND() 将返回一个float,所以你还没有真正解决你的问题。您可以尝试使用自己的 CLR 函数,但我也不知道那会成功。
【解决方案2】:

在纯 T-SQL 中,LOGEXP 使用 float 类型(8 个字节)进行操作,该类型只有 15-17 significant digits。如果对足够大的值求和,即使最后 15 位数字也会变得不准确。你的数据是numeric(22,6),所以15位有效数字是不够的。

POWER 可以返回 numeric 类型,精度可能更高,但这对我们来说用处不大,因为 LOGLOG10 无论如何都只能返回 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 会丢失精度。使用这些内置函数,我认为您无能为力。


所以,答案是 - 使用 C# decimal 类型的 CLR(不是 double),它支持更高的精度(28-29 位有效数字)。您的原始 SQL 类型 numeric(22,6) 将适合它。而且你不需要LOG/EXP 的技巧。


哎呀。我试图制作一个计算 Product 的 CLR 聚合。它在我的测试中有效,但仅作为一个简单的聚合,即

这行得通:

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;

关键字“ORDER”附近的语法不正确。

搜索发现此connect item,已关闭为“不会修复”和此question


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

question 详细讨论了运行 SUM 的计算以及 Paul White shows in his answer 如何编写有效计算运行 SUM 的 CLR 函数。这将是编写一个计算运行产品的函数的良好开端。

请注意,他使用了不同的方法。 Paul 没有制作自定义 聚合 函数,而是制作了一个返回表格的函数。该函数将原始数据读入内存并执行所有需要的计算。

通过使用您选择的编程语言在您的客户端实现这些计算,可能会更容易实现所需的效果。只需阅读整个表格并计算客户端上的运行产品。如果在服务器上计算的运行产品是更复杂的计算中的中间步骤,那么创建 CLR 函数是有意义的,该计算将进一步聚合数据。


又想到了一个想法。

查找提供高精度 LogExp 函数的第三方 .NET 数学库。制作这些 标量 函数的 CLR 版本。然后使用EXP + LOG + SUM() Over (Order by)的方法,其中SUM是内置的T-SQL函数,支持Over (Order by)ExpLog是自定义CLR函数,返回的不是float,而是高-精度decimal.

请注意,高精度计算也可能很慢。在查询中使用 CLR 标量函数也可能会使其变慢。

【讨论】:

  • 您能否添加一个 CLR 示例来执行此操作。我对CLR一无所知。如果我实施 CLR,这将是我们办公室的一大变化。
  • 您必须使用 .NET 语言、C# 或 VB.NET....不是那么难,但也不是那么容易....
  • @Antonio,我添加了一个CLR代码,但事实证明,它不能用于计算运行产品。
【解决方案3】:

对于您的数据,您可以四舍五入到大倍数:

--720000000000000 must be multiple of 600

select
   round( 719999999999998/600,  0 ) * 600

--result: 720000000000000

Test it at SQLFiddle

create TABLE T 
  (
     PAR_COLUMN INT,
     PERIOD     INT,
     VALUE      NUMERIC(22, 6)
  ) 
INSERT INTO T VALUES 
(1,601,10.1 ),    --<--- I put decimals just to test!
(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)

查询 1

with T1 as (
SELECT *,
       Exp(Sum(Log(Abs(NULLIF(VALUE, 0))))
             OVER(
               PARTITION BY PAR_COLUMN
               ORDER BY PERIOD)) AS CUM_MUL,
       VALUE AS CUM_MAX1,
       LAG( VALUE , 1, 1.) 
             OVER(
               PARTITION BY PAR_COLUMN
               ORDER BY PERIOD ) AS CUM_MAX2,
       LAG( VALUE , 2, 1.) 
             OVER(
               PARTITION BY PAR_COLUMN
               ORDER BY PERIOD ) AS CUM_MAX3
FROM   T )
select PAR_COLUMN,  PERIOD,  VALUE, 
       ( round( ( CUM_MUL  / ( CUM_MAX1 * CUM_MAX2 * CUM_MAX3) ) ,6) 
         * 
         cast( ( 1000000 * CUM_MAX1 * CUM_MAX2 * CUM_MAX3) as bigint )
       ) / 1000000.
       as CUM_MUL
FROM T1

Results

| PAR_COLUMN | PERIOD | VALUE |         CUM_MUL |
|------------|--------|-------|-----------------|
|          1 |    601 |  10.1 |            10.1 | --ok! because my data
|          1 |    602 |    20 |             202 |
|          1 |    603 |    30 |            6060 |
|          1 |    604 |    40 |          242400 |
|          1 |    605 |    50 |        12120000 |
|          1 |    606 |    60 |       727200000 |
|          2 |    601 |   100 |             100 |
|          2 |    602 |   200 |           20000 |
|          2 |    603 |   300 |         6000000 |
|          2 |    604 |   400 |      2400000000 |
|          2 |    605 |   500 |   1200000000000 |
|          2 |    606 |   600 | 720000000000000 |

注意我 x1000000 可以不使用小数

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-01-07
    • 2018-11-01
    • 1970-01-01
    • 2016-11-03
    • 2020-09-17
    • 2023-03-18
    • 2014-02-18
    相关资源
    最近更新 更多