c#SqlDecimal在小数字的乘法中翻转符号

时间:2017-01-30 14:28:16

标签: c# sql-server sqlclr

似乎我遇到了more woes'我最喜欢的数据类型'SqlDecimal。 我想知道这是否应该被认为是一个错误。

当我在SQL中乘以两个小数字时,我得到了预期的结果。当我通过SQLCLR函数运行相同的数字时,结果非常令人惊讶。

c#c​​ode:

using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

namespace TestMultiplySQLDecimal
{
    public static class Multiplier
    {
        [SqlFunction(DataAccess=DataAccessKind.None, IsDeterministic = true,IsPrecise = true)]
        public static SqlDecimal Multiply(SqlDecimal a, SqlDecimal b)
        {
            if (a.IsNull || b.IsNull) return SqlDecimal.Null;
            return a*b;
        }
    }
}

SQL代码:

USE tempdb
GO
IF DB_ID('test') IS NOT NULL DROP DATABASE test
GO
CREATE DATABASE test
GO
USE test
GO

CREATE ASSEMBLY TestMultiplySQLDecimal 
FROM 'C:\Users\tralalalaa\Documents\visual studio 2015\Projects\TestMultiplySQLDecimal\TestMultiplySQLDecimal\bin\Release\TestMultiplySQLDecimal.dll'
WITH PERMISSION_SET = SAFE
GO

CREATE FUNCTION dbo.fn_multiply(@a decimal(38,8), @b decimal(18,8)) 
RETURNS decimal(38,8)
EXTERNAL NAME TestMultiplySQLDecimal.[TestMultiplySQLDecimal.Multiplier].Multiply
GO
DECLARE @a decimal(38, 8),
        @b decimal(18, 8),
        @c decimal(38, 8),
        @f decimal(38, 8)

SELECT @a = -0.00000450,
       @b = 0.193,
       @c = NULL,
       @f = NULL

SELECT @c = @a * @b,
       @f = dbo.fn_multiply(@a, @b)

SELECT multiply = null, c = @c, f = @f

结果是: c = -0.00000100 f = +0.00000100

我知道“绝对”的差异是“最小的”而且我“淡化”了更大的错误,将其归咎于“四舍五入的差异”......但是很难向客户解释负面时间的积极结果正。 毕竟,T-SQL支持它很好......

我可以尝试使用十进制(28,8)代替十进制(38,8)解决它但我会遇到其他(完全不相关)的问题然后= /

以下控制台应用程序出现同样的问题,无需涉及SQL Server / SQLCLR:

using System;
using System.Data.SqlTypes;

namespace PlayAreaCSCon
{
    class Program
    {
        static void Main(string[] args)
        {
            var dec1 = new SqlDecimal(-0.00000450d);
            var dec2 = new SqlDecimal(0.193d);
            dec1 = SqlDecimal.ConvertToPrecScale(dec1, 38, 8);
            dec2 = SqlDecimal.ConvertToPrecScale(dec2, 18, 8);
            Console.WriteLine(dec1 * dec2);
            Console.ReadLine();
        }
    }
}

打印0.000001

2 个答案:

答案 0 :(得分:5)

我认为该错误位于第1550 of SqlDecimal行左右:

ret = new SqlDecimal(rgulRes, (byte)culRes, (byte)ResPrec, 
                     (byte)ActualScale, fResPositive);

if (ret.FZero ())
     ret.SetPositive();

ret.AssertValid();

ret.AdjustScale(lScaleAdjust, true);

return ret;

它首先使用final scale参数构造一个新的十进制数。接下来根据传入的构造函数参数检查结果是否为“零”。

然后,在声明一切有效后,它会执行比例调整。

在执行FZero检查时,结果类似于-0.0000008685。我们知道 final 比例将为6,因为我们处于结果scale and precision的极限。那么,前6位数字都是零。

仅在此之后,当调整比例时,需要考虑舍入并将1移动到最后的小数位。

这是一个错误。遗憾的是,decimal的SQL Server本机实现的源代码不公开,因此我们无法将其与SqlDecimal的托管实现进行比较,以了解它们的相似程度以及原始版本如何避免同样的错误。

答案 1 :(得分:2)

虽然T-SQL和.NET实现之间的行为差​​异“令人不安”并且确实指向了一个错误,而@ Damien_The_Unbeliever的fine investigative work可能很好地确定了这种行为的原因(很难验证)目前,由于SqlDecimal实现中存在大量代码,而其中一些使用double进行不精确的计算以绕过.NET不支持超过28位数,因此可能存在更大的代码问题在这里被忽略:两个答案(即c = -0.00000100 f = +0.00000100)都是错误的!也许我们不应该如此仓促地确定“天空是格子花呢”和“天空是波尔卡点缀”之间的胜利者; - )

在这种情况下,我们可能需要在目标中更加务实,更多地了解Decimal操作的局限性,并扩大我们的测试范围。

首先,虽然为一组未知输入保留最大数据类型空间似乎是一个好主意,但使用DECIMAL(38, y)类似于对所有字符串使用NVARCHAR(MAX)。是的,它通常适合你投入的任何东西,但有后果。考虑到如何计算得到的精度和比例的性质,在十进制运算中还有一个额外的结果,尤其是,当给予“比例”(即8位数)这么小的空间并且非常倍增时小数字。含义:如果您不打算使用小数点左侧的全部30位数字(即DECIMAL(38, 8)),则不要指定DECIMAL(38, 8)。对于输入参数,只需指定每个值允许的最大大小。鉴于两者都低于0,使用DECIMAL(20, 18)(或甚至DECIMAL(18, 8))之类的东西不仅非常灵活,而且会产生正确的结果。或者,如果你真的需要允许大值,那么通过指定DECIMAL(38, 28)之类的东西给小数点右侧的数字(即“缩放”)提供更多的空间,其中左边有10位数字。小数点,右边28位。

所有内容的原始DECIMAL(38,8)

DECLARE @a DECIMAL(38, 8),
        @b DECIMAL(38, 8),
        @c DECIMAL(38, 8);

SELECT @a = -0.00000450,
       @b = 0.193;

SELECT @c = @a * @b;

SELECT @a * @b AS [RawCalculation], @c AS [c];

返回:

RawCalculation    c
-0.000001         -0.00000100

使用DECIMAL(18,8)

DECLARE @a DECIMAL(18, 8),
        @b DECIMAL(18, 8),
        @c DECIMAL(38, 18),
        @d DECIMAL(20, 18),
        @e DECIMAL(38, 8);

SELECT @a = -0.00000450,
       @b = 0.193;

SELECT @c = @a * @b,
       @d = @a * @b,
       @e = @a * @b;

SELECT @a * @b AS [RawCalculation], @c AS [c], @d AS [d], @e AS [e];

返回:

RawCalculation          c                         d                         e
-0.0000008685000000     -0.000000868500000000     -0.000000868500000000     -0.00000087

使用DECIMAL(38,28)

DECLARE @a DECIMAL(38, 28),
        @b DECIMAL(38, 28),
        @c DECIMAL(38, 18),
        @d DECIMAL(20, 18),
        @e DECIMAL(38, 8);

SELECT @a = -0.00000450,
       @b = 0.193;

SELECT @c = @a * @b,
       @d = @a * @b,
       @e = @a * @b;

SELECT @a * @b AS [RawCalculation], @c AS [c], @d AS [d], @e AS [e];

返回:

RawCalculation          c                         d                            e
-0.00000086850000000    -0.000000868500000000     -0.000000868500000000        -0.00000087

.NET示例代码

以下代码基于@Damien添加到问题中的示例代码。我扩展它以进行额外的测试,以显示精度和比例的变化如何影响计算,并在每一步输出各种属性。请注意,.NET中十进制的文字表示使用Mm,而不是d(尽管它在此测试中没有区别):decimal (C# Reference) < / p>

using System;
using System.Data.SqlTypes;

namespace SqlDecimalMultiplication
{
    class Program
    {
        private static void DisplayStuffs(SqlDecimal Dec1, SqlDecimal Dec2)
        {
            Console.WriteLine("1 ~ {0}", Dec1.Value);
            Console.WriteLine("1 ~ Precision: {0}; Scale: {1}; IsPositive: {2}", Dec1.Precision, Dec1.Scale, Dec1.IsPositive);
            Console.WriteLine("2 ~ {0}", Dec2.Value);
            Console.WriteLine("2 ~ Precision: {0}; Scale: {1}; IsPositive: {2}", Dec2.Precision, Dec2.Scale, Dec2.IsPositive);

            Console.Write("\nRESULT:    ");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine(Dec1 * Dec2);
            Console.ResetColor();

            return;
        }

        static void Main(string[] args)
        {
            var dec1 = new SqlDecimal(-0.00000450m);
            var dec2 = new SqlDecimal(0.193m);

            Console.WriteLine("=======================\n\nINITIAL:");
            DisplayStuffs(dec1, dec2);


            dec1 = SqlDecimal.ConvertToPrecScale(dec1, 38, 8);
            dec2 = SqlDecimal.ConvertToPrecScale(dec2, 18, 8);

            Console.WriteLine("=======================\n\nAFTER (38, 8) & (18, 8):");
            DisplayStuffs(dec1, dec2);


            dec1 = SqlDecimal.ConvertToPrecScale(dec1, 18, 8);

            Console.WriteLine("=======================\n\nAFTER (18, 8) & (18, 8):");
            DisplayStuffs(dec1, dec2);


            dec1 = SqlDecimal.ConvertToPrecScale(dec1, 38, 28);
            dec2 = SqlDecimal.ConvertToPrecScale(dec2, 38, 28);

            Console.WriteLine("=======================\n\nAFTER (38, 28) & (38, 28):");
            DisplayStuffs(dec1, dec2);

            Console.WriteLine("=======================");
            //Console.ReadLine();
        }
    }
}

返回:

=======================

INITIAL:
1 ~ -0.00000450
1 ~ Precision: 8; Scale: 8; IsPositive: False
2 ~ 0.193
2 ~ Precision: 3; Scale: 3; IsPositive: True

RESULT:    -0.00000086850
=======================

AFTER (38, 8) & (18, 8):
1 ~ -0.00000450
1 ~ Precision: 38; Scale: 8; IsPositive: False
2 ~ 0.19300000
2 ~ Precision: 18; Scale: 8; IsPositive: True

RESULT:    0.000001
=======================

AFTER (18, 8) & (18, 8):
1 ~ -0.00000450
1 ~ Precision: 18; Scale: 8; IsPositive: False
2 ~ 0.19300000
2 ~ Precision: 18; Scale: 8; IsPositive: True

RESULT:    -0.0000008685000000
=======================

AFTER (38, 28) & (38, 28):
1 ~ -0.0000045000000000000000000000
1 ~ Precision: 38; Scale: 28; IsPositive: False
2 ~ 0.1930000000000000000000000000
2 ~ Precision: 38; Scale: 28; IsPositive: True

RESULT:    -0.00000086850000000
=======================

结论

虽然SqlDecimal中可能存在错误,但如果正确指定输入参数的精度和比例,则可能不会遇到错误。当然,除非您确实需要输入值为38位,但大多数用例都不需要。

另外,在上面的段落中突出显示“输入参数”的原因是为了表明返回值自然应该是更高的精度(和比例)所以以适应某些操作导致的精度和/或比例的增加。因此,将DECIMAL(38, 28)DECIMAL(38,18)保留为返回值的数据类型没有任何问题。

相关说明:

对于SQLCLR UDF(即标量函数),如果它涵盖所有输入参数,请不要使用此模式:

if (a.IsNull || b.IsNull) return SqlDecimal.Null;

如果想法是返回NULL如果任何输入参数是NULL,那么您应该在CREATE FUNCTION语句中使用以下选项:

WITH RETURNS NULL ON NULL INPUT

因为这将完全避免调用.NET代码!