SQL Server 错误或功能?小数转换

Posted

技术标签:

【中文标题】SQL Server 错误或功能?小数转换【英文标题】:SQL Server bug or feature? Decimal numbers conversion 【发布时间】:2018-01-25 12:43:51 【问题描述】:

在开发过程中遇到了相当奇怪的 SQL Server 行为。在这里,对于完全相同的数字,我们有完全相同的公式。唯一的区别是我们如何获得这个数字(4.250)。来自表、临时表、变量表或硬编码值。舍入和铸造在所有情况下都是完全一样的。

-- normal table
CREATE TABLE [dbo].[value]
(
[val] [decimal] (5, 3) NOT NULL
) 
INSERT INTO [value] VALUES (4.250 )
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM [value] AS pr

-- inline query from normal table
SELECT * FROM (SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM [value] AS pr) a

-- record without table
SELECT ROUND(CAST(4.250 * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val

-- table variable
DECLARE @value AS TABLE (
val  [decimal] (5, 3)
);

INSERT INTO @value VALUES (4.250 )

SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM @value

-- temp table
CREATE TABLE #value
(
    val  [decimal] (5, 3)
)
INSERT INTO #value VALUES (4.250 )
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM #value AS pr

-- all records together
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM [value] AS pr
UNION ALL
SELECT ROUND(CAST(4.250 * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
UNION ALL
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM @value
UNION ALL
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM #value AS pr

DROP TABLE #value;
DROP TABLE [dbo].[value];

结果是:

【问题讨论】:

仅供参考 - 关于添加新标签,我看到在我有权访问的所有版本(2008R2 - 2016)上都有相同的行为 【参考方案1】:

来自SQL Server data types页面

当您使用 +、-、*、/ 或 % 算术运算符执行 int、smallint、tinyint 或 bigint 的隐式或显式转换 浮点数、实数、小数或数字数据类型的常量值, SQL Server 在计算数据类型时应用的规则和 表达式结果的精度取决于是否 查询是否自动参数化。

因此,查询中的类似表达式有时会产生 不同的结果。当查询未自动参数化时,常量 值首先转换为数字,其精度刚刚大 足以保存常量的值,然后再转换为 指定的数据类型。例如,常数值 1 转换为 numeric (1, 0),常量值250转换为numeric (3, 0)

当查询被自动参数化时,常量值总是 在转换为最终数据之前转换为numeric (10, 0) 类型。当涉及 / 运算符时,不仅可以将结果类型的 相似查询之间的精度不同,但结果值可以 也不同。例如,自动参数化的结果值 包含表达式 SELECT CAST (1.0 / 7 AS float) 的查询 将不同于不是相同查询的结果值 自动参数化,因为自动参​​数化查询的结果 将被截断以适应 numeric (10, 0) 数据类型。

注意:

numeric (10, 0) 等价于INT

在上面的示例中,当被除数和除数都是整数时,类型被视为INT,例如INT / INT = INT

另一方面,如果其中一个类型被强制为“正确的”NUMERIC 类型,则表达式被视为NUMERIC( 10, 0 ) / NUMERIC( 10, 0 ) = NUMERIC( 21, 11 )。有关如何计算结果类型的说明,请参阅:Precision, scale, and Length (Transact-SQL)。

例子:

EXEC sp_describe_first_result_set N'SELECT 1 as a, 7 as b, 1 / 7 AS Result'
EXEC sp_describe_first_result_set N'SELECT 1 as a, CONVERT( NUMERIC( 10, 0 ), 7 ) as b, CONVERT( INT, 1 ) / CONVERT( NUMERIC( 10, 0 ), 7 ) AS a'

注意: NUMERIC 数据类型只有固定的小数位数(刻度)来存储小数。当除法产生具有(无限)长小数部分的结果时,这一点变得很重要,例如1 / 3 必须被截断以适应类型。

自动参数化

来自Microsoft White Paper:

... 只有那些参数值不影响的 SQL 语句 查询计划选择是自动参数化的。

SQL Server 的 LPE(语言处理和执行)组件 自动参数化 SQL 语句。当 QP(查询处理器)组件 意识到文字常量的值不会影响查询计划 选择,它声明 LPE 的自动参数化尝试是“安全的”,并且 自动参数化进行;否则,自动参数化是 声明为“不安全”并被中止。

如果查询处理器认为查询“不安全”,查询仍会执行,但缓存的计划仅适用于该特定查询。

上述文章详细描述了不符合自动参数化条件的语句类型。

SQLTeam 文章提供了一个很好的总结:

单表 - 无联接 没有 IN 子句 没有联合 没有选择进入 没有查询提示 没有 DISTINCT 或 TOP 没有全文、链接服务器或表变量 没有子查询 无分组方式 WHERE 子句中没有 没有功能 没有使用 FROM 子句的 DELETE 或 UPDATE 参数值不会影响计划

OP 案例

结果的差异归结为12是否被自动参数化并被视为INT/NUMERIC( 10, 0 ),因此被视为NUMERIC( 2, 0 )。这将直接影响舍入前结果的精度(小数位数):decimal(19,16)decimal(11,8)

输入参数:

-- Note: on my machine "parameterization" option does not have any effect on below example
SELECT CONVERT( decimal (5, 3), 4.250 ) AS a, -- the type is explicitly defined in the table
    0.01 AS b -- always becomes NUMERIC( 2, 2 )
    12 AS c -- will either become NUMERIC( 2, 0 ) or NUMERIC( 10, 0 ) / INT
EXEC sp_describe_first_result_set N'SELECT CONVERT( decimal (5, 3), 4.250 ) AS a, 0.01 AS b, 12 AS c'

在上述情况下,它被视为INT

您可以“强制”将其视为NUMERIC( 2, 0 )

-- Note: on my machine "parameterization" option does not have any effect on below example
SELECT 0.01 AS b, ( 12 * 0.01 ) AS c
EXEC sp_describe_first_result_set N'SELECT ( 12 * 0.01 ) AS c'
-- Result: 0.12 numeric(5,2)

商品数据类型计算公式:p1 + p2 + 1, s1 + s2

找出起始类型求解:5 = x + 2 + 1, 2 = y + 2 得到2, 0NUMERIC( 2, 0 )

结果的输出类型如下:

-- 12 is NUMERIC( 10, 0 ) / INT
SELECT CONVERT( decimal (5, 3), 4.250 ) * CONVERT( decimal (2, 2), 0.01 ) / CONVERT( decimal(10, 0), 12 )
EXEC sp_describe_first_result_set N'SELECT CONVERT( decimal (5, 3), 4.250 ) * CONVERT( decimal (2, 2), 0.01 ) / CONVERT( decimal(10, 0), 12 )'
-- Result: 0.0035416666666666 decimal(19,16) -> rounding to 9 decimal places: 0.003541667

-- 12 is NUMERIC( 2, 0 )
SELECT CONVERT( decimal (5, 3), 4.250 ) * CONVERT( decimal (2, 2), 0.01 ) / CONVERT( decimal(2, 0), 12 )
EXEC sp_describe_first_result_set N'SELECT CONVERT( decimal (5, 3), 4.250 ) * CONVERT( decimal (2, 2), 0.01 ) / CONVERT( decimal(2, 0), 12 )'
-- Result: 0.00354166 decimal(11,8) -> rounding to 9 decimal places: 0.003541660

要了解如何计算结果类型,请参阅Precision, scale, and Length (Transact-SQL)。

按照Dale Burnett 文章中描述的步骤,获取了 OPs 示例中每个语句的查询计划。检查每个语句的ParameterizedPlanHandle 属性的查询计划。结果如下,可能的原因(参见上面的自动参数化部分)没有自动参数化:

    普通表:自动参数化。请注意 XML 计划中的以下内容:ParameterizedText="(@1 numeric(2,2),@2 int)SELECT round(CONVERT([decimal](15,9),[val]*@1/@2),(9)) [val] FROM [value] [pr]" 内联:没有自动参数化。原因:子查询。请注意,CTE 也不会自动参数化。 没有表:没有自动参数化。原因:不确定。可能太琐碎了。 表变量:没有自动参数化。原因:表变量 Tempt Table:没有自动参数化。原因:不确定。没有明确提及临时表。 “一起”:没有自动参数化。原因:UNION

解决方案

将您的文字和/或中间结果转换为所需的类型以避免意外,例如

SELECT CONVERT( decimal( 12, 7 ), CONVERT( decimal (5, 3), 4.250 ) * CONVERT( decimal (2, 2), 0.01 )) / CONVERT( decimal(2, 0), 12 )
EXEC sp_describe_first_result_set N'SELECT CONVERT( decimal( 12, 7 ), CONVERT( decimal (5, 3), 4.250 ) * CONVERT( decimal (2, 2), 0.01 )) / CONVERT( decimal(2, 0), 12 )'
-- Result: 0.0035416666 decimal(15,10) -> rounding to 9 decimal places: 0.003541660

总结

这个问题是一个复杂的案例:Division of 2 numbers using CAST function in SQL server 2008R2。复杂性源于 SQL Server 可能在不同场景中使用不同的数据类型。

好读

How To Tell If Your Query Has Been Auto-Parameterized (and why wasn’t it?) Plan Caching in SQL Server 2008

【讨论】:

非常丰富的答案。但是,我尝试对前 2 个示例使用 sp_describe_first_result_set(从表中选择 val 并从(从表中选择 val)中选择 *),我得到相同的输出,但查询的结果不同 @DmitrijKultasev - 您需要从示例中删除 CASTROUND 才能查看实际类型。也许我的回答不够明确,但结果的差异来自 在应用最终演员表之前的结果类型的规模差异。 @DmitrijKultasev - 我已经更新了我的答案,希望它能消除混乱。 我可以理解为什么从我的问题中的第三个示例开始会有这种效果。但是它并没有向我解释为什么 select * from tbl 给出的结果与 select * from (select * from tbl) 不同。 @HoneyBadger - 我检查了这篇文章并检查了 OP 的示例代码:对于前两种情况,两个计划都是“TRIVIAL”。看起来这篇文章不完整/具有误导性。我找到了一篇稍微好一点的文章:daleburnett.com/2011/08/…。在我再次更新答案之前,需要更详细地阅读这两篇文章。【参考方案2】:

如果我跑:

SELECT  CAST(pr.val * 0.01 / 12 AS DECIMAL(15, 9)) AS val
,       SQL_VARIANT_PROPERTY(CAST(pr.val * 0.01 / 12 AS DECIMAL(15, 9)), 'BaseType')
FROM    [value] AS pr

返回值0.003541660

如果我跑:

SELECT  CAST(pr.val * 0.01 / 12 AS DECIMAL(15, 9)) AS val
FROM    [value] AS pr

返回值0.003541667

我闻起来很像虫子……

编辑

根据 Bridge 的回答,我也决定看看执行计划。你瞧:

SELECT  CAST(pr.val * 0.01 / 12 AS DECIMAL(15, 9)) AS val
FROM    [value] AS pr
OPTION (RECOMPILE)


-- inline query from normal table
SELECT  a.val
FROM    (
            SELECT  CAST(pr.val * 0.01 / 12 AS DECIMAL(15, 9)) AS val
            FROM    [value] AS pr
        ) AS a
OPTION (RECOMPILE)

两个查询都返回0.003541660。因此,执行计划的重用似乎是“错误”的根源。 (注意:DBCC FREEPROCCACHE 没有相同的结果!)

额外说明:如果我将执行计划保存为 xml,则无论有无OPTION (RECOMPILE),文件都是相同的。

编辑:

如果我将数据库设置为PARAMETERIZATION FORCED,子查询仍然在没有参数的情况下执行。如果我通过显式使用0.0112 作为变量来强制参数化,则返回的值再次相同。我认为 SQL Server 以与预期不同的数据类型定义参数。不过,我无法将结果强制为 0.003541660。 这也解释了为什么OPTION(RECOMPILE) 会产生相同的值:如果使用 RECOMPILE,则转为参数化off。

【讨论】:

我同意似乎发生了一些奇怪的事情 - 看看我的最新编辑,有什么突然出现的吗? @Bridge,请看我的编辑。我认为我们需要对 SQL Server 内部机制有更深入了解的人 @HoneyBadger - 在此处查看大红色警告:docs.microsoft.com/en-us/sql/t-sql/data-types/…【参考方案3】:

这似乎是因为您没有在硬编码该值的任何地方指定数据类型 4.250,以及在表声明和强制转换语句中混合数据类型 decimal(5,3)decimal(15,9)

请注意,在任何地方都指定相同的精度:

-- normal table
CREATE TABLE [dbo].[value]
  (
     [val] DECIMAL(15, 9) NOT NULL
  )

INSERT INTO [value]
SELECT CAST(4.250 AS DECIMAL(15, 9))

SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
FROM   [value] AS pr

-- inline query from normal table
SELECT *
FROM   (SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
        FROM   [value] AS pr) a

-- record without table
SELECT ROUND(CAST(CAST(4.250 AS DECIMAL(15, 9)) * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val

-- table variable
DECLARE @value AS TABLE
  (
     val [DECIMAL] (15, 9)
  );

INSERT INTO @value
SELECT CAST(4.250 AS DECIMAL(15, 9))

SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
FROM   @value

-- temp table
CREATE TABLE #value
  (
     val [DECIMAL] (15, 9)
  )

INSERT INTO #value
SELECT CAST(4.250 AS DECIMAL(15, 9))

SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
FROM   #value AS pr

-- all records together
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
FROM   [value] AS pr
UNION ALL
SELECT ROUND(CAST(CAST(4.250 AS DECIMAL(15, 9)) * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
UNION ALL
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
FROM   @value
UNION ALL
SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val
FROM   #value AS pr

DROP TABLE #value;

DROP TABLE [dbo].[value];

每一行都得到相同的结果:

0.003541667

补充说明:

您可以通过将硬编码数值填充到变体中来测试它是什么数据类型:

DECLARE @var SQL_VARIANT;

SELECT @var = 4.250

SELECT SQL_VARIANT_PROPERTY(@var, 'BaseType'),
       SQL_VARIANT_PROPERTY(@var, 'Precision'),
       SQL_VARIANT_PROPERTY(@var, 'Scale');

这将在我的本地 SQL Server 机器上返回 numeric(4,3)。 (数字和小数是same thing)

编辑 #2:进一步挖掘

仅举第一个例子:

CREATE TABLE [dbo].[value]
(
[val] [decimal] (5, 3) NOT NULL
) 
INSERT INTO [value] VALUES (4.250 )

SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM [value] AS pr

-- inline query from normal table
SELECT * FROM (SELECT ROUND(CAST(val * 0.01 / 12 AS DECIMAL(15, 9)), 9) AS val FROM [value] AS pr) a

DROP TABLE VALUE

进一步挖掘,执行计划有所不同 - 第一条语句被参数化,而子查询版本不是:

如果您查看属性窗口:

它没有列出这些参数的数据类型,但是通过将值 0.0112 填充到变量中来执行相同的技巧,最终数据类型分别为 numeric(2,2)int

如果您将第二条语句中的硬编码值转换为这些数据类型:

SELECT * FROM (SELECT ROUND(CAST(val * CAST(0.01 AS NUMERIC(2,2)) / CAST(12 AS INT) AS DECIMAL(15, 9)), 9) AS val FROM [value] AS pr) a

这两个语句的结果相同。为什么它决定参数化选择而不是子查询,参数的数据类型实际上是什么,以及硬编码值在第二个语句中被视为正常的数据类型......对我来说仍然是一个谜。我们可能需要询问具有 SQL Server 引擎内部知识的人。

【讨论】:

我同意这与数据类型有关。如果您将12 更改为12.0,您也会在任何地方得到相同的结果(0.003541666)。我仍然不明白为什么内联查询会给出不同的结果。 我知道如何在任何地方获得正确的结果。您也可以将 val 乘以 0.010000000000 而不是 0.01。但是它没有回答为什么当表定义相同时我会得到不同的结果? @DmitrijKultasev 我明白你的意思 - 为什么选择表返回的值与从同一语句中选择子查询时的值不同...... 我认为您在参数化方面走在了正确的轨道上。如果我将数据库设置为PARAMETERIZATION FORCED,子查询仍然在没有参数的情况下执行。如果我通过显式使用0.0112 作为变量来强制参数化,则返回的值再次相同。我认为 SQL Server 以与预期不同的数据类型定义参数。不过,我无法将结果强制为0.003541660。 See here @Bridge - 这应该使您的发现正式化:docs.microsoft.com/en-us/sql/t-sql/data-types/…。 When you use the +, -, *, /, or % arithmetic operators to perform implicit or explicit conversion of int, smallint, tinyint, or bigint constant values to the float, real, decimal or numeric data types, the rules that SQL Server applies when it calculates the data type and precision of the expression results differ depending on whether the query is autoparameterized or not.

以上是关于SQL Server 错误或功能?小数转换的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 将变量 varchar 字段转换为货币/小数/带小数位的东西

sql server2000中CONVERT中各个参数的意思

如何在 SQL Server 2000/2005/2008 中使用 FLOAT 转换小数位数

SQL Server 2017:功能包缺失文件

安装sql server 2008时,到功能选择时提示验证码错误,属性不匹配!想请教

sql server 中数据类型numeric我用小数点后两位,可是小数点前没有0