c# SqlDecimal 在小数乘法中翻转符号
c# SqlDecimal flipping sign in multiplication of small numbers
看来我运行正在使用 'my most favorite datatype' SqlDecimal 进入 more woes。
我想知道这是否应该被视为一个错误。
当我在 SQL 中乘以两个小数时,我得到了预期的结果。当我通过 SQLCLR 函数 运行 相同的数字时,结果令人惊讶。
c#代码:
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
我知道 'absolute' 的区别是 'minimal' 我有 "played down" 更大的错误归咎于 "rounding differences"...但这很难解释对客户来说,消极乘以积极会产生积极的结果。
毕竟,T-SQL 支持得很好...
我可以尝试使用 decimal(28,8) 而不是 decimal(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
我认为错误位于行 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;
它首先使用最终的比例参数构造一个新的小数。接下来根据传入的构造函数参数检查结果是否为 "zero"。
然后,在断言一切都有效后,它会执行比例调整。
执行 FZero 检查时,结果类似于 -0.0000008685
。我们知道 最终 比例将为 6,因为我们已达到结果 scale and precision 的极限。好吧,前6位全为零。
只是在那之后,在调整刻度时,它才考虑四舍五入并将1
移动到最后的小数位。
这是一个错误。不幸的是,decimal
的 SQL 服务器本机实现的源代码并未公开,因此我们无法将其与 SqlDecimal
的托管实现进行比较,以了解它们之间的相似程度和方式原版避免了同样的错误。
虽然 T-SQL 和 .NET 实现之间的行为差异是 "troubling" 并且确实指向一个错误,而 @Damien_The_Unbeliever 的 可能很好地确定了这种行为的原因(目前很难验证,因为 SqlDecimal
实现中有很多代码,其中一些代码使用 double
来绕过 .NET 不支持的不精确计算超过 28 位),这里很可能忽略了一个更大的问题:两个答案(即 c = -0.00000100 f = +0.00000100
)都是错误的!也许我们不应该那么仓促地决定"The sky is plaid"和"The sky is polka-dotted"之间的赢家;-)
在这种情况下,我们可能需要在目标上更加务实一些,更多地了解 Decimal 运算的局限性,并扩大我们的测试范围。
首先,虽然为未知输入集保留最大数据类型 space 似乎是个好主意,但使用 DECIMAL(38, y)
类似于使用 NVARCHAR(MAX)
所有字符串。是的,它通常可以容纳你扔给它的任何东西,但也有后果。考虑到结果精度和小数位数的计算方式的性质,小数运算还有一个额外的结果,尤其是,因为 "scale"(即 8 位数字)的空间太小) 并且乘以非常小的数字。意思是:如果你不打算使用小数点左边的 30 位数字的完整范围(即 DECIMAL(38, 8)
),那么不要指定 DECIMAL(38, 8)
。对于输入参数,只需指定每个值允许的最大大小。鉴于两者都低于 0,使用类似 DECIMAL(20, 18)
(甚至 DECIMAL(18, 8)
)的东西不仅非常灵活,而且会产生正确的结果。或者,如果您确实需要允许较大的值,则通过指定 DECIMAL(38, 28)
之类的内容,为小数点右侧的数字(即 "scale")提供更多 space小数点左边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];
Returns:
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];
Returns:
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];
Returns:
RawCalculation c d e
-0.00000086850000000 -0.000000868500000000 -0.000000868500000000 -0.00000087
.NET示例代码
以下代码基于@Damien 添加到问题中的示例代码。我将其扩展以进行额外的测试,以显示精度和比例的变化如何影响计算,并在每一步输出各种属性。请注意,.NET 中小数的字面表示使用 M
或 m
,而不是 d
(尽管在本次测试中没有影响):decimal (C# Reference)
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();
}
}
}
Returns:
=======================
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
中可能存在错误,但如果指定 输入参数的精度和小数位数 "properly",您应该不会遇到它.当然,除非您确实确实需要传入值的 38 位数字,但大多数用例永远不需要它。
此外,在上面的段落中突出显示“输入参数”的原因是表明 return 值 自然应该是更大的精度(和比例),以适应由于某些操作导致的精度 and/or 比例的增加。因此,将 DECIMAL(38, 28)
或 DECIMAL(38,18)
保留为 return 值的数据类型没有任何问题。
相关说明:
对于 SQLCLR UDF(即标量函数),如果它涵盖 所有 个输入参数,请勿使用此模式:
if (a.IsNull || b.IsNull) return SqlDecimal.Null;
如果想法是 return a NULL
如果输入参数的 any 是 NULL
,那么你应该使用下面的CREATE FUNCTION
语句中的选项:
WITH RETURNS NULL ON NULL INPUT
因为这将避免完全调用 .NET 代码!
看来我运行正在使用 'my most favorite datatype' SqlDecimal 进入 more woes。 我想知道这是否应该被视为一个错误。
当我在 SQL 中乘以两个小数时,我得到了预期的结果。当我通过 SQLCLR 函数 运行 相同的数字时,结果令人惊讶。
c#代码:
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
我知道 'absolute' 的区别是 'minimal' 我有 "played down" 更大的错误归咎于 "rounding differences"...但这很难解释对客户来说,消极乘以积极会产生积极的结果。 毕竟,T-SQL 支持得很好...
我可以尝试使用 decimal(28,8) 而不是 decimal(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
我认为错误位于行 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;
它首先使用最终的比例参数构造一个新的小数。接下来根据传入的构造函数参数检查结果是否为 "zero"。
然后,在断言一切都有效后,它会执行比例调整。
执行 FZero 检查时,结果类似于 -0.0000008685
。我们知道 最终 比例将为 6,因为我们已达到结果 scale and precision 的极限。好吧,前6位全为零。
只是在那之后,在调整刻度时,它才考虑四舍五入并将1
移动到最后的小数位。
这是一个错误。不幸的是,decimal
的 SQL 服务器本机实现的源代码并未公开,因此我们无法将其与 SqlDecimal
的托管实现进行比较,以了解它们之间的相似程度和方式原版避免了同样的错误。
虽然 T-SQL 和 .NET 实现之间的行为差异是 "troubling" 并且确实指向一个错误,而 @Damien_The_Unbeliever 的 SqlDecimal
实现中有很多代码,其中一些代码使用 double
来绕过 .NET 不支持的不精确计算超过 28 位),这里很可能忽略了一个更大的问题:两个答案(即 c = -0.00000100 f = +0.00000100
)都是错误的!也许我们不应该那么仓促地决定"The sky is plaid"和"The sky is polka-dotted"之间的赢家;-)
在这种情况下,我们可能需要在目标上更加务实一些,更多地了解 Decimal 运算的局限性,并扩大我们的测试范围。
首先,虽然为未知输入集保留最大数据类型 space 似乎是个好主意,但使用 DECIMAL(38, y)
类似于使用 NVARCHAR(MAX)
所有字符串。是的,它通常可以容纳你扔给它的任何东西,但也有后果。考虑到结果精度和小数位数的计算方式的性质,小数运算还有一个额外的结果,尤其是,因为 "scale"(即 8 位数字)的空间太小) 并且乘以非常小的数字。意思是:如果你不打算使用小数点左边的 30 位数字的完整范围(即 DECIMAL(38, 8)
),那么不要指定 DECIMAL(38, 8)
。对于输入参数,只需指定每个值允许的最大大小。鉴于两者都低于 0,使用类似 DECIMAL(20, 18)
(甚至 DECIMAL(18, 8)
)的东西不仅非常灵活,而且会产生正确的结果。或者,如果您确实需要允许较大的值,则通过指定 DECIMAL(38, 28)
之类的内容,为小数点右侧的数字(即 "scale")提供更多 space小数点左边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];
Returns:
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];
Returns:
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];
Returns:
RawCalculation c d e
-0.00000086850000000 -0.000000868500000000 -0.000000868500000000 -0.00000087
.NET示例代码
以下代码基于@Damien 添加到问题中的示例代码。我将其扩展以进行额外的测试,以显示精度和比例的变化如何影响计算,并在每一步输出各种属性。请注意,.NET 中小数的字面表示使用 M
或 m
,而不是 d
(尽管在本次测试中没有影响):decimal (C# Reference)
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();
}
}
}
Returns:
=======================
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
中可能存在错误,但如果指定 输入参数的精度和小数位数 "properly",您应该不会遇到它.当然,除非您确实确实需要传入值的 38 位数字,但大多数用例永远不需要它。
此外,在上面的段落中突出显示“输入参数”的原因是表明 return 值 自然应该是更大的精度(和比例),以适应由于某些操作导致的精度 and/or 比例的增加。因此,将 DECIMAL(38, 28)
或 DECIMAL(38,18)
保留为 return 值的数据类型没有任何问题。
相关说明:
对于 SQLCLR UDF(即标量函数),如果它涵盖 所有 个输入参数,请勿使用此模式:
if (a.IsNull || b.IsNull) return SqlDecimal.Null;
如果想法是 return a NULL
如果输入参数的 any 是 NULL
,那么你应该使用下面的CREATE FUNCTION
语句中的选项:
WITH RETURNS NULL ON NULL INPUT
因为这将避免完全调用 .NET 代码!