似乎我遇到了more woes'我最喜欢的数据类型'SqlDecimal。 我想知道这是否应该被认为是一个错误。
当我在SQL中乘以两个小数字时,我得到了预期的结果。当我通过SQLCLR函数运行相同的数字时,结果非常令人惊讶。
c#code:
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
答案 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中十进制的文字表示使用M
或m
,而不是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代码!