web-dev-qa-db-ja.com

SQL Serverのバグまたは機能? 10進数の変換

開発中に、非常に奇妙なSQLServerの動作に直面しました。ここでは、まったく同じ数に対してまったく同じ式があります。唯一の違いは、この数値(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

20

これは、テーブル宣言とキャストステートメントでデータ型decimal(5,3)decimal(15,9)を混合するとともに、その値をハードコーディングしたすべての場所で4.250のデータ型を指定していないためと思われます。

どこでも同じ精度を指定することに注意してください。

_-- 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');
_

これにより、ローカルSQLServerボックスにnumeric(4,3)が返されます。 (数値と小数は 同じもの

編集#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.01_と_12_をバリアントに詰め込んで同じトリックを実行すると、データ型numeric(2,2)intそれぞれ。

2番目のステートメントのハードコードされた値をこれらのデータ型にキャストする場合:

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

両方のステートメントで同じ結果が得られます。サブクエリではなく選択をパラメータ化することを決定した理由、パラメータの実際のデータ型、およびハードコードされた値が2番目のステートメントで通常どおりに扱われるデータ型...私には謎のままです。 SQLServerエンジンの内部知識を持っている人に尋ねる必要があるでしょう。

14
Bridge

私が実行した場合:

_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_が返されます。

私にとってはバグのようなにおいがします...

編集

ブリッジの回答をもとに、私も実行計画を見てみることにしました。見よ、見よ:

_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.01_と_12_を明示的に使用してパラメーター化を強制すると、戻り値は再び同じになります。 SQL Serverは、予想とは異なるデータ型でパラメーターを定義していると思います。ただし、結果を0.003541660に強制することはできませんでした。これは、OPTION(RECOMPILE)が同じ値になる理由も説明しています。RECOMPILEが使用されている場合、パラメーター化は off になります。

11
HoneyBadger

From SQL Serverデータ型 ページ

+、-、*、/、または%算術演算子を使用して、int、smallint、tinyint、またはbigint定数値をfloat、real、decimal、またはnumericデータ型に暗黙的または明示的に変換する場合、SQLServerの規則データ型を計算するときに適用され、式の結果の精度は、クエリが自動パラメータ化されているかどうかによって異なります。

したがって、クエリ内の同様の式は、異なる結果を生成する場合があります。クエリが自動パラメータ化されていない場合、定数値は最初に数値に変換されます。その精度は、指定されたデータ型に変換する前に、定数の値を保持するのに十分な大きさです。たとえば、定数値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

一方、タイプの1つが「適切な」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'

注:NUMERICデータ型には、小数を格納するための小数点以下の桁数(スケール)が固定されています。これは、除算が(無限に)長い小数部分で結果を生成する場合に重要になります。タイプに合わせて切り捨てる必要がある1/3。

OPケース

結果の違いは、12がINT/NUMERIC( 10, 0 )またはNUMERIC( 2, 0 )として扱われるかどうかに要約されます。これは、結果の精度(小数点以下の桁数)に直接影響するためです: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 )

結果の出力タイプは次のようになります。

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

結果タイプの計算方法を確認するには、 精度、スケール、および長さ(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

概要:

この質問は、次の複雑なケースです。 SQL Server 2008R2のCAST関数を使用した2つの数値の除算 。 SQLServerがさまざまなシナリオでさまざまなデータ型を使用する可能性があるという事実に起因する複雑さ。

簡単なパラメータ化に関する一言

クエリが自動パラメータ化される場合とされない場合に実際に言及している単純なパラメータ化に関する記事は1つしか見つかりませんでした( http://www.sqlteam.com )。

注:この記事は2007年のものであるため、最新ではない可能性があります。

SQL Serverでは、単純なパラメーター化を使用してパラメーター化できるクエリの種類に次の制限があります。

  • 単一テーブル–結合なし
  • IN句なし
  • UNIONなし
  • SELECTINTOなし
  • クエリのヒントはありません
  • DISTINCTまたはTOPはありません
  • フルテキスト、リンクされたサーバー、またはテーブル変数はありません
  • サブクエリはありません
  • GROUPBYなし
  • WHERE句に<>がありません
  • 機能なし
  • FROM句を使用したDELETEまたはUPDATEはありません
  • パラメータ値は計画に影響を与えることはできません

TechNet --Simple Parameterization 記事に情報がありません。

TechNet-強制パラメータ化 いくつかの情報はありますが、強制パラメータ化に適用されます

10
Alex