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

时间:2017-08-17 07:48:28

标签: 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];

结果是:

enter image description here

3 个答案:

答案 0 :(得分:14)

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

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

-- 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

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

execution plans

如果查看属性窗口:

enter image description here

它没有列出这些参数的数据类型,但是将值0.0112填充到变量中的相同技巧最终会使用数据类型numeric(2,2)和{{1分别。

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

int

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

答案 1 :(得分:10)

如果我跑:

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

答案 2 :(得分:10)

来自SQL Server data types页面

  

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

     

因此,查询中的类似表达式有时会产生   不同的结果。当查询不是自动参数化时,常量   value首先转换为numeric,其精度很大   足以在转换为之前保持常量的值   指定的数据类型。例如,将常数值1转换为   numeric (1, 0),常数值250转换为numeric (3, 0)

     

当查询自动参数化时,常量值始终为   在转换为最终数据之前转换为numeric (10, 0)   类型。当涉及/运算符时,结果类型不仅可以   类似查询的精度不同,但结果值可以   也有所不同。例如,自动参数化的结果值   包含表达式SELECT CAST (1.0 / 7 AS float)的查询   将与不相同的查询的结果值不同   autoparameterized,因为自动参​​数化查询的结果   将被截断以适合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必须被截断以适合该类型。

OPs case

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

输入参数:

ROUND

在上述情况下,它被视为-- 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, 0

结果的输出类型如下:

NUMERIC( 2, 0 )

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

解决方案

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

-- 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

要点:

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

关于简单参数化的一个词

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

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

  

SQL Server对哪些类型的查询设置了以下限制   可以使用简单参数化参数化:

     
      
  • 单表 - 无JOIN
  •   
  • No IN clause
  •   
  • No UNION
  •   
  • No SELECT INTO
  •   
  • 无查询提示
  •   
  • 没有DISTINCT或TOP
  •   
  • 没有全文链接服务器或表变量
  •   
  • 没有子查询
  •   
  • No GROUP BY
  •   
  • 否<>在WHERE子句
  • 中   
  • 无功能
  •   
  • 没有使用FROM子句删除或更新
  •   
  • 参数值不能影响计划
  •   

TechNet - Simple Parameterization文章没有任何信息。

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