SQL Server错误或功能?十进制数转换

Dmi*_*sev 20 sql sql-server sql-server-2008-r2 sql-server-2014 sql-server-2016

在开发过程中遇到了相当奇怪的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];
Run Code Online (Sandbox Code Playgroud)

结果是:

在此输入图像描述

Bri*_*dge 14

这似乎是因为您没有在硬编码该值的任何地方指定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];
Run Code Online (Sandbox Code Playgroud)

每行得到相同的结果:

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');
Run Code Online (Sandbox Code Playgroud)

这将numeric(4,3)在我的本地SQL Server框中返回.(数字和小数是一回事)

编辑#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
Run Code Online (Sandbox Code Playgroud)

进一步挖掘后,执行计划是不同的 - 第一个语句是参数化的,而子查询版本不是:

执行计划

如果查看属性窗口:

在此输入图像描述

它不会列出这些参数的数据类型,但带馅的价值观做同样的伎俩0.01,并12进入一个变种与数据类型结束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
Run Code Online (Sandbox Code Playgroud)

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


Hon*_*ger 11

如果我跑:

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
Run Code Online (Sandbox Code Playgroud)

0.003541660返回值.

如果我跑:

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

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)
Run Code Online (Sandbox Code Playgroud)

两个查询都返回0.003541660.因此,执行计划的重用似乎是"错误"的起源.(注意:DBCC FREEPROCCACHE结果不一样!)

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

编辑:

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

  • @HoneyBadger - 请在此处查看大红色警告:https://docs.microsoft.com/en-us/sql/t-sql/data-types/int-bigint-smallint-and-tinyint-transact-sql (4认同)

Ale*_*lex 11

SQL Server数据类型页面

当您使用+, - ,*,/或%算术运算符执行int,smallint,tinyint或bigint常量值到float,real,decimal或numeric数据类型的隐式或显式转换时,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 ).请参阅:精度,小数位数和长度(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'
Run Code Online (Sandbox Code Playgroud)

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

OP案例

结果的差异归结为12是否被视为INT/ NUMERIC( 10, 0 )NUMERIC( 2, 0 )因为这将直接影响结果的精度(小数位数):decimal(19,16)decimal(11,8).我已删除INTNUMERIC( 2, 0 )运行以显示计算中使用的实际类型.

输入参数:

-- 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'
Run Code Online (Sandbox Code Playgroud)

在上述情况下,它被视为p1 + p2 + 1, s1 + s2.

你可以"强迫"它被视为5 = x + 2 + 1, 2 = y + 2:

-- 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)
Run Code Online (Sandbox Code Playgroud)

计算产品数据类型的公式:2, 0.

找出起始类型解决:NUMERIC( 2, 0 )得到ParameterizedPlanHandleieParameterizedText="(@1 numeric(2,2),@2 int)SELECT round(CONVERT([decimal](15,9),[val]*@1/@2),(9)) [val] FROM [value] [pr]"

结果的输出类型如下:

-- 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
Run Code Online (Sandbox Code Playgroud)

要查看计算结果类型的方式,请参阅精度,比例和长度(Transact-SQL).

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

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
Run Code Online (Sandbox Code Playgroud)

摘要:

这个问题是一个复杂的案例:在SQL Server 2008R2中使用CAST函数划分2个数字.由于SQL Server可能在不同的场景中使用不同的数据类型,因此复杂性很高.

关于简单参数化的一个词

我只能找到一篇关于简单参数化的文章(http://www.sqlteam.com),当一个查询不能自动参数化时,它会实际提到.

注意:该文章来自2007年,因此可能不是最新的.

SQL Server对可以使用简单参数化参数化的查询类型进行以下限制:

  • 单表 - 无JOIN
  • 没有IN条款
  • 没有UNION
  • 没有选择
  • 没有查询提示
  • 没有DISTINCT或TOP
  • 没有全文链接服务器或表变量
  • 没有子查询
  • 没有GROUP BY
  • WHERE子句中没有<>
  • 没有功能
  • 使用FROM子句没有DELETE或UPDATE
  • 参数值不能影响计划

TechNet - 简单参数化文章没有任何信息.

TechNet - 强制参数化确实有一些信息,但它适用于强制参数化