SQLで199.96-0 = 200なのはなぜですか?


84

奇妙な請求書を受け取っているクライアントがいます。私はコアの問題を切り分けることができました:

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 200 what the?
SELECT 199.96 - (0.0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96)) -- 199.96

SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 199.96

-- It gets weirder...
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 0
SELECT (0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 0
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 0

-- so... ... 199.06 - 0 equals 200... ... right???
SELECT 199.96 - 0 -- 199.96 ...NO....

誰か手がかりがありますか、一体何がここで起こっているのですか?つまり、それは確かに10進数のデータ型と関係がありますが、実際には頭を包むことはできません...


数値リテラルがどのデータ型であるかについて多くの混乱があったので、私は実際の行を表示することにしました。

PS.SharePrice - (CAST((@InstallmentCount - 1) AS DECIMAL(19, 4)) * CAST(FLOOR(@InstallmentPercent * PS.SharePrice) AS DECIMAL(19, 4))))

PS.SharePrice DECIMAL(19, 4)

@InstallmentCount INT

@InstallmentPercent DECIMAL(19, 4)

DECIMAL(19, 4)外部コンテキストに適用する前に、異なるタイプのオペランドを持つ各操作の結果が明示的にキャストされていることを確認しました。

それにもかかわらず、結果は残り200.00ます。


これで、コンピューターで実行できる要約サンプルを作成しました。

DECLARE @InstallmentIndex INT = 1
DECLARE @InstallmentCount INT = 1
DECLARE @InstallmentPercent DECIMAL(19, 4) = 1.0
DECLARE @PS TABLE (SharePrice DECIMAL(19, 4))
INSERT INTO @PS (SharePrice) VALUES (599.96)

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * PS.SharePrice),
  1999.96)
FROM @PS PS

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(599.96 AS DECIMAL(19, 4))),
  1999.96)
FROM @PS PS

-- 1996.96
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * 599.96),
  1999.96)
FROM @PS PS

-- Funny enough - with this sample explicitly converting EVERYTHING to DECIMAL(19, 4) - it still doesn't work...
-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))
FROM @PS PS

今、私は何かを持っています...

-- 2000
SELECT
  IIF(1 = 2,
  FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))

-- 1999.9600
SELECT
  IIF(1 = 2,
  CAST(FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) AS INT),
  CAST(1999.96 AS DECIMAL(19, 4)))

とにかく、floorは整数を返すことになっています。何が起きてる?:-D


私は今、それを本当に本質にまで煮詰めることができたと思います:-D

-- 1.96
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (36, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2.0
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (37, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (38, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

4
@Sliverdust 199.96 -0は200に等しくありません。これらすべてのキャスト、および浮動小数点への暗黙的な変換を伴うフロアは、精度が低下することが保証されています。
Panagiotis Kanavos 2018

1
@Silverdustは、テーブルからのものである場合のみ。式のリテラルとして、それはおそらくfloat
PanagiotisKanavos18年

1
ああ...そして戻りFloor()ませint元の式と同じ型を返しますが、小数点部分は削除されています。それ以外の場合、IIF()関数の結果は優先順位が最も高いタイプになります(docs.microsoft.com/en-us/sql/t-sql/functions/…)。したがって、intにキャストする2番目のサンプルでは、​​numeric(19,4)としての単純なキャストが優先されます。
Joel Coehoorn 2018

1
すばらしい答えですが(SQLバリアントのメタデータを調べることができると誰が知っていましたか?)、2012年には期待される結果(199.96)が得られました。
ベンジャミンモスコビッツ2018

2
私は、MS SQLとあまり慣れていないんだけど、私がしなければならないので、私は...私の注意を引いたようにすぐにそれらのキャスト操作のすべてを見て、と言わなければならないこれをリンクする誰もがなければならないので、これまで使用していないことがfloatハンドル通貨にING-ポイントの種類を。
code_dredd 2018

回答:


78

何が起こっているのかを確認できるように、これを少しアンラップすることから始める必要があります。

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

次に、SQLServerが減算操作の両側で使用しているタイプを正確に確認しましょう。

SELECT  SQL_VARIANT_PROPERTY (199.96     ,'BaseType'),
    SQL_VARIANT_PROPERTY (199.96     ,'Precision'),
    SQL_VARIANT_PROPERTY (199.96     ,'Scale')

SELECT  SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'BaseType'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Precision'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Scale')

結果:

数値52
数値381

そう199.96ですnumeric(5,2)、そして長いほどFloor(Cast(etc))ですnumeric(38,1)

結果として得られる減算演算の精度とスケール規則(つまり、:)は次のe1 - e2ようになります。

精度: max(s1、s2)+ max(p1-s1、p2-s2)+ 1
スケール: max(s1、s2)

それは次のように評価されます:

精度: max(1,2)+ max(38-1、5-2)+ 1 => 2 + 37 + 1 => 40
スケール: max(1,2)=> 2

ルールのリンクを使用numeric(38,1)して、最初にどこから来たのかを把握することもできます(ヒント:2つの精度19の値を乗算しました)。

だが:

  • 結果の精度とスケールの絶対最大値は38です。結果の精度が38を超えると、38に減少し、対応するスケールが減少して、結果の整数部分が切り捨てられないようにします。乗算や除算などの場合、オーバーフローエラーが発生する可能性はありますが、小数の精度を維持するためにスケール係数は減少しません。

おっと。精度は40です。精度を下げる必要があります。精度を下げると、常に最下位桁が切り捨てられるため、スケールも小さくなります。式の最終的な結果の型は、になりますnumeric(38,0)。これは、に199.96丸められ200ます。

あなたは、おそらく移動と統合することにより、この問題を解決することができますCAST()し、大きな表現の内側からの操作を1 CAST()式全体の結果を中心に。したがって、この:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

になる:

SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))

アウターキャストも外すかもしれません。

ここでは、期待される結果ではなく、現在実際に持っている精度とスケールに一致するタイプを選択する必要があることを学びます。SQL Serverは算術演算中にこれらの型を変更してオーバーフローを回避しようとするため、高精度の数値を選択するだけでは意味がありません。


詳しくは:


20

次のステートメントに関連するデータ型に注意してください。

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))))
  1. NUMERIC(19, 4) * NUMERIC(19, 4)NUMERIC(38, 7)(以下を参照)
    • FLOOR(NUMERIC(38, 7))NUMERIC(38, 0)(以下を参照)
  2. 0.0 です NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0) です NUMERIC(38, 1)
  3. 199.96 です NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)NUMERIC(38, 1)(以下を参照)

これは、あなたがで終わる理由を説明200.0小数ではなく、ゼロの後に1桁)の代わりに、199.96

ノート:

FLOOR指定された数式以下の最大の整数を返し、結果は入力と同じ型になります。INTの場合はINT、FLOATの場合はFLOAT、NUMERIC(x、y)の場合はNUMERIC(x、0)を返します。

アルゴリズムによると:

Operation | Result precision                    | Result scale*
e1 * e2   | p1 + p2 + 1                         | s1 + s2
e1 - e2   | max(s1, s2) + max(p1-s1, p2-s2) + 1 | max(s1, s2)

*結果の精度とスケールの絶対最大値は38です。結果の精度が38を超えると、38に減少し、対応するスケールが減少して、結果の整数部分が切り捨てられないようにします。

この説明には、加算および乗算演算内でスケールがどのように正確に縮小されるかについての詳細も含まれています。その説明に基づいて:

  • NUMERIC(19, 4) * NUMERIC(19, 4)NUMERIC(39, 8)クランプされていますNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)NUMERIC(40, 1)クランプされていますNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)NUMERIC(40, 2)クランプされていますNUMERIC(38, 1)

これがJavaScriptでアルゴリズムを実装する私の試みです。結果をSQLServerと照合しました。それはあなたの質問の本質的な部分に答えます。

// https://docs.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql?view=sql-server-2017

function numericTest_mul(p1, s1, p2, s2) {
  // e1 * e2
  var precision = p1 + p2 + 1;
  var scale = s1 + s2;

  // see notes in the linked article about multiplication operations
  var newscale;
  if (precision - scale < 32) {
    newscale = Math.min(scale, 38 - (precision - scale));
  } else if (scale < 6 && precision - scale > 32) {
    newscale = scale;
  } else if (scale > 6 && precision - scale > 32) {
    newscale = 6;
  }

  console.log("NUMERIC(%d, %d) * NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_add(p1, s1, p2, s2) {
  // e1 + e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2) + 1;
  var scale = Math.max(s1, s2);

  // see notes in the linked article about addition operations
  var newscale;
  if (Math.max(p1 - s1, p2 - s2) > Math.min(38, precision) - scale) {
    newscale = Math.min(precision, 38) - Math.max(p1 - s1, p2 - s2);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_union(p1, s1, p2, s2) {
  // e1 UNION e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2);
  var scale = Math.max(s1, s2);

  // my idea of how newscale should be calculated, not official
  var newscale;
  if (precision > 38) {
    newscale = scale - (precision - 38);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

/*
 * first example in question
 */

// CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))
numericTest_mul(19, 4, 19, 4);

// 0.0 * FLOOR(...)
numericTest_mul(1, 1, 38, 0);

// 199.96 * ...
numericTest_add(5, 2, 38, 1);

/*
 * IIF examples in question
 * the logic used to determine result data type of IIF / CASE statement
 * is same as the logic used inside UNION operations
 */

// FLOOR(DECIMAL(38, 7)) UNION CAST(1999.96 AS DECIMAL(19, 4)))
numericTest_union(38, 0, 19, 4);

// CAST(1.0 AS DECIMAL (36, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(36, 0, 19, 4);

// CAST(1.0 AS DECIMAL (37, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(37, 0, 19, 4);

// CAST(1.0 AS DECIMAL (38, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(38, 0, 19, 4);

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.