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) | 38 | 30 | 8 | 99,999,999 |
decimal(18,2) | 18 | 2 | 16 | 9,999,999,999,999,999 |
decimal(38,2) | 38 | 2 | 36 | 10^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 桁 | 14 | decimal(14,2) |
| 16 桁 | 2 桁 | 18 | decimal(18,2) |
| 20 桁 | 2 桁 | 22 | decimal(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… 正常に変換できたレコードは値が入り、失敗したものは NULLIsOutOfRange… 設計上の許容範囲を超えたレコードに 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 を決めるときは、次のように上から順に決めていくと迷いにくくなります。
- その金額が表すものは何か?(取引金額、残高、税額、レート…)
- ビジネス上、理論的に取りうる最大値はいくらか?
- 小数は何桁まで必要か?(通貨なら 2 桁、レートなら 4 桁など)
- その最大値を整数部の桁数に換算する
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 であっても安定したデータ基盤を構築できます。

コメント