SQL Serverのfloatをdecimalに変換するとオーバーフローする原因と安全な対処法【Arithmetic overflow error対策】

SQL Serverでリンクサーバーから取引データを取り込んだとき、「float列をdecimal/numericに変換したらArithmetic overflow errorが出て困る」という相談は意外なほど多くあります。とくに指数表記(-1.369258488045704E+15)のような大きな金額が混ざると、一見正しく見える定義でも簡単にオーバーフローします。本記事では、エラーの正体と安全な変換手順、そして実務で役立つスキーマ設計の考え方を体系的に整理します。

目次

float列からdecimalに変換するとオーバーフローする典型的な例

まず、実際に起きている状況を整理します。

  • リンクサーバーから取引データを取り込んでいる
  • 金額列は float 型としてリンクサーバーから見えている
  • 値には -1.369258488045704E+15 のような指数表記が含まれる
  • ステージングテーブルでは float のまま受け取れている
  • ファクト表に移送する際、金額を「小数2桁」にしたくて decimal(38,30) などへ変換したところ…
Arithmetic overflow error converting float to data type numeric.

というエラーが発生してしまう、というパターンです。

さらにサプライヤー側は「うちのシステムでは金額は money 型の想定です」と言っているものの、実際に来ているデータは桁数が多く、どう見ても money の上限を超えている——このギャップがトラブルの温床になります。

decimal(p,s)の意味を取り違えると簡単にオーバーフローする

この手のトラブルでいちばん多いのが、decimal(p, s) の意味の取り違えです。

  • p(precision)…合計桁数(整数部+小数部)
  • s(scale)…小数部の桁数
  • 整数部の桁数 … p − s

つまり、decimal(38,30) は

  • 合計 38 桁
  • うち小数 30 桁
  • 整数部は 8 桁(= 38 − 30)

しか持てません。

ところが、問題になっている値 -1.369258488045704E+15 を展開すると、だいたい次のような値になります。

-1,369,258,488,045,704

これは整数部だけで 16 桁あります。decimal(38,30) に入れようとしても、整数部は 8 桁までしかないため「入りきらない → Arithmetic overflow error」となるわけです。

decimal定義と表現可能な整数部の関係

型定義合計桁数 (p)小数桁 (s)整数部桁数 (p − s)最大整数部(おおよそ)
decimal(38,30)3830899,999,999
decimal(18,2)182169,999,999,999,999,999
decimal(38,2)3823610^36 – 1 ≈ 1e36

「小数 30 桁あれば十分に精度が高いはず」と思って decimal(38,30) を選ぶと、整数部がたった 8 桁しかない点に気づきにくく、今回のような大きな金額を扱うシステムでは簡単にオーバーフローしてしまいます。

money型・decimal(19,4)の上限と、例の値が入らない理由

サプライヤーが「money 型で扱っているつもり」と言う場合、SQL Server 側の代表的な金額型は次の2つです。

型内部の精度/スケール表現可能な範囲(おおよそ)備考
money精度 19、小数 4 桁相当± 922,337,203,685,477.5807
(約 ±9.22×10^14)
小数 4 桁固定
decimal(19,4)精度 19、小数 4± 999,999,999,999,999.9999
(約 ±1.0×10^15)
money 互換で使われがち

今回の例の値 1.369×10^15 は約 1,369,000,000,000,000 となり、money の上限(約 9.22×10^14)を超えています。また、decimal(19,4) の上限(約 9.999×10^14)もオーバーしています。

つまり、サプライヤー側が「money のつもり」と言っている一方で、実際に流れてきている値は money/decimal(19,4) の設計範囲を越えている、という矛盾が生じている可能性が高いと言えます。

小数2桁で扱いたい場合のdecimal定義の考え方

ビジネス要件として「小数 2 桁で良い(通貨など)」という前提があるなら、precision(合計桁数)は「必要な整数部の桁数+2」で設計するのが基本です。

例:今回の値を安全に収めたい場合

  • 最大で 16 桁の整数部が現れうる(例の値が 16 桁)
  • 小数部は 2 桁ほしい

ということは

  • precision = 16 + 2 = 18

となり、decimal(18,2) を選ぶのが自然です。

より余裕を持たせたい場合は、例えば decimal(20,2) や decimal(28,2) などを選んでも構いません。極端な例として decimal(38,2) まで広げれば、金融システムでもそうそうオーバーフローすることはありませんが、その分インデックスやストレージの負荷は増すため、闇雲に精度を上げれば良いわけでもありません。

想定最大整数桁必要な小数桁推奨 precision推奨型例
12 桁2 桁14decimal(14,2)
16 桁2 桁18decimal(18,2)
20 桁2 桁22decimal(22,2)

float列からdecimalへの安全な変換パターン

ここからは実際の T‑SQL コードレベルでの「安全な変換パターン」を整理します。

float列から直接decimal(18,2)へ変換する

ステージングテーブルに float で取り込んだ金額を、ファクト表に decimal(18,2) で格納したい場合の最もシンプルな書き方です。

SELECT
    CAST([Amount] AS decimal(18,2)) AS Amount_2dp
FROM dbo.Staging;

このとき、[Amount] が decimal(18,2) の範囲を超えていると、やはりオーバーフローが発生します。そのため、後述のような範囲チェックや TRY_CAST を組み合わせるとより安全になります。

文字列(指数表記を含む)からの変換:varchar → float → decimal

リンクサーバーや外部ファイルからのインポートでは、金額が varchar で渡され、さらに指数表記(1.23E+05)を含んでいるケースもよくあります。

SQL Server は、指数表記の varchar を decimal へ直接キャストできません。そのため、いったん float を経由する 2 段階変換が必要になります。

SELECT
    CAST(CAST([AmountText] AS float) AS decimal(18,2)) AS Amount_2dp
FROM dbo.Staging;

変換失敗(変な文字列や範囲外)を NULL にして流したい場合は、TRY_CAST を使うと安全です。

SELECT
    CAST(TRY_CAST([AmountText] AS float) AS decimal(18,2)) AS Amount_2dp
FROM dbo.Staging;
  • TRY_CAST が失敗した場合 … NULL が返る
  • その後の CAST(... AS decimal) は NULL をそのまま返すのでエラーにならない

このパターンを使えば、「おかしな文字列」や「指数表記の異常値」が混じっていても、取り急ぎはファクト表に安全に流し込みつつ、後から NULL レコードを重点的に洗い出す、といった運用が可能になります。

丸めルールを明示する:ROUNDの使い方

金額の小数 2 桁をどう丸めるか(四捨五入/切り捨て/切り上げ)は、会計上かなり重要な仕様です。SQL Server の ROUND 関数を使えば、丸めルールを明示的に記述できます。

一般的には、まず十分な精度の decimal に変換し、その上で ROUND するのがおすすめです。

SELECT
    ROUND(CAST([Amount] AS decimal(18,4)), 2) AS Amount_2dp
FROM dbo.Staging;

ここでは

  • いったん decimal(18,4) に変換(小数部を 4 桁確保)
  • その後 ROUND(..., 2) で小数 2 桁に丸める

という流れになっています。途中で小数部を多めに確保しておくことで、不自然な丸め誤差を減らすことができます。

オーバーフローや変換エラーを検知しながら移送するガード付きクエリ

実務では「変換できるレコードだけ通し、怪しいレコードは別テーブルに逃がす」運用がよく使われます。そのための典型的なクエリ例です。

SELECT
    TRY_CAST([Amount] AS decimal(18,2)) AS Amount_2dp,
    CASE
        WHEN [Amount] >  9999999999999999.99
          OR [Amount] < -9999999999999999.99 THEN 1
        ELSE 0
    END AS IsOutOfRange
FROM dbo.Staging;
  • Amount_2dp … 正常に変換できたレコードは値が入り、失敗したものは NULL
  • IsOutOfRange … 設計上の許容範囲を超えたレコードに 1 を立てるフラグ

この結果を使って、

  • IsOutOfRange = 0 かつ Amount_2dp IS NOT NULL … 正常レコードとしてファクト表へ移送
  • IsOutOfRange = 1 または Amount_2dp IS NULL … エラーレコードとして別キューに退避し、後で調査

という二段構えにしておけば、バッチ全体がオーバーフローで止まる事態を防ぎつつ、データ品質も担保できます。

ステージングとファクトでデータ型を分ける設計指針

データウェアハウスや分析用DBでは、「ステージング」と「ファクト/ディメンション」でデータ型の考え方を分けるのが定石です。

層目的データ型の考え方
ステージング供給元の値を「ありのまま」受けるリンクサーバーやファイルから受けた型をほぼそのまま使う 場合によっては float や varchar でもOK あくまで「生データ置き場」と割り切る
ファクト/ディメンション集計・分析・レポートの基盤金額・数量は decimal で統一 集計単位ごとに precision/scale を明確に定義 float は原則として使わない

今回のように

  • ステージング … float で受ける
  • ファクト … 設計した decimal(18,2) 等へ変換する

という分離は理にかなっています。重要なのは、ファクト表の decimal 定義を「ビジネス上の最大値+小数桁数」に基づいて決めることです。

ビジネス要件からprecisionを決める手順

precision を決めるときは、次のように上から順に決めていくと迷いにくくなります。

  1. その金額が表すものは何か?(取引金額、残高、税額、レート…)
  2. ビジネス上、理論的に取りうる最大値はいくらか?
  3. 小数は何桁まで必要か?(通貨なら 2 桁、レートなら 4 桁など)
  4. その最大値を整数部の桁数に換算する
  5. precision = 整数部桁数 + 小数桁数 で決定する

例えば、「最大 999 京円(= 9.99×10^17)までありえる売上金額を小数 2 桁で扱う」なら

  • 最大整数部 ≒ 18 桁
  • 小数 2 桁
  • precision = 18 + 2 = 20

となるので、decimal(20,2) といった定義が合理的です。

事前に最大値・最大桁数を調査するクエリ

「そんな最大値、ビジネス部門に聞いても今すぐ出てこない」ということも多いはずです。その場合は、まず既存データの実測値から「どのくらいの桁数が実際に使われているか」を調査するのが有効です。

SELECT
    MAX(ABS([Amount])) AS MaxAbsValue,
    MAX(
        CASE
            WHEN [Amount] <> 0
                THEN FLOOR(LOG10(ABS([Amount]))) + 1
            ELSE 1
        END
    ) AS MaxIntegerDigits
FROM dbo.Staging;
  • MaxAbsValue … 絶対値の最大値(ざっくりした上限の把握)
  • MaxIntegerDigits … 整数部の桁数の最大値

ここで得られた MaxIntegerDigits に、必要な小数桁数を足せば、precision の目安が立ちます。

  • 例えば MaxIntegerDigits = 16、小数 2 桁なら precision = 18

もちろん、将来的なビジネス拡大を見込んで 1〜2 桁余裕を持たせるのもアリです。

リンクサーバー経由のfloatでよくある落とし穴

リンクサーバー経由で「なぜか金額が float で見えてしまう」ケースには、いくつかのパターンがあります。

  • 元のDB(Oracle / MySQL / PostgreSQL 等)の数値型が、ドライバーの都合で float にマッピングされている
  • 中間に ETL ツールやファイル連携が入り、そこで「とりあえず float・double にしている」
  • 元システムの仕様変更により、想定以上の桁数・範囲の金額が流れ始めた

サプライヤーが「money のつもり」と言っていても、実際に流れてきているデータが money の範囲を超えていれば、それはもはや「仕様違反」です。この記事で紹介したような

  • 最大値・最大桁数の事前調査
  • TRY_CAST による変換エラーの検知
  • IsOutOfRange フラグでの範囲外検知

といった手段で「実データが本当に仕様通りか?」を測定し、必要に応じてサプライヤー側と仕様すり合わせを行うことが重要です。

実務で使えるチェックリスト

最後に、「float → decimal 変換でハマらないためのチェックリスト」をまとめます。

項目チェック内容
decimal(p,s)の理解整数部桁数 = p − s になっているか?
最大値の把握実データの最大値・最大整数桁数をクエリで確認したか?
precision決定「最大整数桁数+小数桁数」でprecisionを決めたか?
丸め仕様ROUNDで小数2桁への丸めルールをコードに明示したか?
変換失敗の扱いTRY_CASTやIsOutOfRangeで異常値を検知し、別テーブルへ避難させているか?
スキーマ分離ステージングではfloat/文字列も許容し、ファクトではdecimalで統一しているか?
サプライヤーとの仕様合意型・範囲・桁数・丸め規則をドキュメントとして合意しているか?

まとめ:float→decimalへの変換は「型選定」と「検知仕組み」で安定させる

本記事で扱った内容を、改めて整理します。

  • オーバーフローの主因は、decimal(p,s) の precision/scale 設定ミスであることが多い
  • decimal(38,30) は小数部こそ多いが、整数部はたった 8 桁しかなく、大きな金額はすぐにあふれる
  • money / decimal(19,4) は約 10^15 までしか扱えず、1.369×10^15 のような値は範囲外になる
  • 小数2桁の金額なら、必要な整数部桁数+2をprecisionにとる(例:16桁なら decimal(18,2))
  • 文字列で指数表記が来る場合は、varchar → float → decimal の二段階変換を行い、TRY_CASTで失敗をNULLに逃がす
  • ROUNDを併用して丸めルールを明示すると、会計上のトラブルを防げる
  • TRY_CASTと範囲チェック(IsOutOfRange)を組み合わせれば、バッチを止めずに異常値だけを隔離できる
  • ステージングでは float を許容しつつ、ファクト表は decimal で一貫管理するのがDWH設計の王道
  • 実データの最大値をクエリで調査し、ビジネスとすり合わせながらprecisionを決定することが重要

「とりあえず decimal(38,30) にしておけば安全」という発想は、実はもっとも危険な選択肢のひとつです。扱う金額の意味と最大値、必要な小数桁数を一度きちんと言語化し、それに基づいて型を設計しておくことで、リンクサーバー経由の float であっても安定したデータ基盤を構築できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次