
金額が1円ずれる・数が化ける|丸め誤差と桁あふれの追い方
「請求書の合計と、管理画面の合計が1円合わないんですが……」
経理の方からそう言われて、計算のコードを開いてみる。足し算と掛け算しかしていない。テストでも合っていた。それなのに、ある注文だけ1円ずれる。たった1円なのに、どこから手をつければいいのか分からなくて、画面の前で手が止まる。数字のバグは、そういう静かな困り方をさせるものです。
これは、あなたの計算が雑だったわけではありません。コンピュータの数字には「小数をぴったり表せない」「どこかで丸めている」「入る桁に上限がある」という、ふだんは見えない性質があります。ずれは、その性質がどこかで顔を出しただけのことがほとんどです。
この記事では、金額や件数がずれる・化けるバグを追う順番を、小さく分けて一緒に整理していきます。
結論:数値のバグは、次の4つの順で追います。①ずれの「型」を見分ける(小さなずれ/端数処理の違い/大きな数が化ける・上限で止まる)、②ずれた値を「正しい値」と並べて、差を数字で出す、③値の通り道(入力 → 計算 → 保存 → 表示・出力)のどこでずれたかを区間で切る、④境目の値で再現してから直す。直し方の基本は、金額は小数を使わず「整数(円)」か「10進数の型」で持つ、丸める場所と方法を1か所に決める、型の上限に余裕を持たせるの3つです。
なお、関数の丸め方や型の上限は、言語・DB・バージョンで変わります。ここで持ち帰ってほしいのは順番と観点です。手元の環境の公式ドキュメントで確かめ、必ず検証環境で試してから直してください。
何が起きているか:数字には「見えない3つの性質」がある
数値のずれは、たいてい次の3つのどれかから生まれます。
- 小数はぴったり表せない(浮動小数点の誤差):多くの言語の小数(float / double)は、内部では2進数で持っています。そのため 0.1 のような値も、ほんのわずかに違う値として扱われます。たとえば
0.1 + 0.2は、JavaScriptやPythonでは0.30000000000000004になります。表示の設定で0.3に見える言語もありますが、中身は同じです。 - どこかで丸めている(端数処理の違い):四捨五入・切り捨て・切り上げ、さらに「ちょうど半分(.5)のときにどちらへ寄せるか」は、関数によって違います。Pythonの
round(2.5)は2(偶数に寄せる丸め)ですが、PHPのround(2.5)やJavaScriptのMath.round(2.5)は3です。明細ごとに丸めるか、合計してから丸めるかでも結果が変わります。 - 入る桁に上限がある(桁あふれ):DBの整数型には上限があります。MySQLやPostgreSQLの
INT(符号付き)は最大 2,147,483,647(約21億)です。JavaScriptの数値は 9,007,199,254,740,991 を超えると、整数を正確に表せなくなります。表計算ソフトも、長い数字の末尾を丸めてしまうことがあります。
一つひとつは小さな性質です。ただ、値が画面から入り、計算され、DBに保存され、帳票やCSVで出ていくあいだに、何度もこの性質をくぐります。どこか1か所でずれれば、最後の数字がずれる。それだけのことです。
よくある症状は、次のような形で現れます。
- 1円・1件・0.01だけずれる:合計が合わない、税額が帳票と画面で違う、割引後の金額が電卓と違う。
- 小数が長くなる・おかしくなる:
19.999999999のような値が画面やCSVに出る。比較if (a == b)がなぜか成り立たない。 - 大きな数が化ける:CSVを開いたら注文番号の末尾が
0になっている。APIで受け取ったIDが、1つ隣の番号に変わっている。 - ある日から急に失敗する:特定のテーブルだけ、ある日から登録がエラーになる(連番が型の上限に届いた)。
数値のずれを追う4つの手順

① ずれの「型」を見分ける
最初に、症状がどの型に当てはまるかを見ます。型が分かると、疑う場所がぐっと絞れます。
| 症状 | 疑う型 | まず見る場所 |
|---|---|---|
| 1円・0.01だけずれる、明細と合計が合わない | 端数処理の違い | 丸めている関数と、丸めるタイミング(明細ごとか合計か) |
19.999999 のような値、== の比較が成り立たない | 浮動小数点の誤差 | 金額や数量を float / double で持っている箇所 |
長い番号の末尾が 0 になる、IDが隣の番号に変わる | 表示・受け渡し側の桁あふれ | 表計算ソフト、JavaScript、JSONの受け渡し |
| ある日から登録だけエラーになる、値が最大値で止まる | DBの型の上限 | 連番(ID)や集計値を入れている列の型 |
「1円ずれる」と「番号が化ける」は、同じ“数字のバグ”に見えても、原因の場所がまったく違います。ここを分けておくだけで、遠回りがだいぶ減ります。
② ずれた値を「正しい値」と並べて、差を数字で出す
次に、ずれた結果と、手計算や電卓で出した正しい値を横に並べます。差がいくつかを、数字で出しておくのがポイントです。
- 差が「1」や「0.01」:端数処理の違いがいちばん怪しい。
- 差が「0.000000001」のようなごく小さな値:浮動小数点の誤差。
- 差が大きく、値が「切りのいい最大値」に張りついている:桁あふれ。
DBに入っている値を、計算し直した値と並べるSQLを1本作っておくと、ずれている行だけを一覧にできます。
-- 明細から計算し直した合計と、保存されている合計が違う注文だけを出す(閲覧だけ。テーブル名・列名は環境に合わせて)
SELECT o.id, o.total_amount, SUM(d.price * d.qty) AS recalculated
FROM orders o
JOIN order_details d ON d.order_id = o.id
GROUP BY o.id, o.total_amount
HAVING o.total_amount <> SUM(d.price * d.qty);
ずれている行が見えたら、ずれる行に共通する条件を探します。「明細が3行以上」「割引クーポンを使った」「数量が端数」など、共通点が見つかれば、それが再現の条件になります(バグを再現して原因を特定する手順)。
③ 値の通り道のどこでずれたかを、区間で切る
数値は、入力 → 計算 → 保存 → 表示・出力の順に通っていきます。ずれた注文を1件選び、各区間で値を書き出して、どこから値が変わったかを探します。
- 入力:画面やAPIから受け取った直後の値は合っているか。文字列から数値に変える時点で、桁が落ちていないか。
- 計算:掛け算・割引・税計算のあと、丸める前の値はいくつか。どの関数で、どの順番で丸めているか。
- 保存:DBの列の型は何か(
INT/DECIMAL(10,2)/FLOATなど)。小数点以下の桁が、列に入りきらず丸められていないか。 - 表示・出力:画面・帳票・CSVに出すときに、もう一度丸めたり、表計算ソフトが勝手に形を変えたりしていないか。
区間ごとに値を出すには、計算の前後に一時的にログを仕込むのが確実です(改修時にログを仕込んで原因を追う)。DBの列の型は、次のように確かめられます。
-- 列の型を確かめる(MySQLの例。PostgreSQLなら \d テーブル名)
SHOW COLUMNS FROM orders;
連番の上限が気になるときは、いまの最大値と型の上限を並べて見ます。
-- いまの最大IDを確かめる(INT 符号付きの上限は 2147483647)
SELECT MAX(id) FROM orders;
上限の半分を超えていたら、すぐに困ることはなくても、次の改修の候補としてメモしておく価値があります。
ここで「計算までは合っていたのに、CSVを表計算ソフトで開いたら化けていた」と分かることも、よくあります。その場合、プログラムは悪くありません。長い番号を数値として読み込んで、15桁を超えた部分が 0 に丸められたり、先頭の 0 が消えたりしているだけです。番号は文字列として扱うよう、出力側か開き方を変えれば済みます。
④ 境目の値で再現してから直す
ずれる区間が分かったら、境目になる値を使って、検証環境で再現します。
- 端数処理:ちょうど
.5になる金額、明細が複数行で一つひとつ端数が出る組み合わせ。 - 浮動小数点:
0.1・0.2・1.005のような、2進数でぴったり表せない値。 - 桁あふれ:型の上限ちょうど、上限 +1、JavaScriptで扱う16桁以上のID。
再現できたら、直し方を決めます。基本は次の3つです。
- 金額は小数で持たない:円なら整数(
INT/BIGINT)で、小数が必要ならDECIMALなどの10進数の型で持ちます。プログラム側も、言語の10進数型(PythonのDecimal、PHPのBCMathなど)を使うと、2進数の誤差を避けられます。 - 丸める場所と方法を1か所に決める:「明細ごとに丸める」か「合計してから丸める」か、「四捨五入」か「切り捨て」かを決め、丸めは共通の関数1つに寄せます。消費税なら、インボイス(適格請求書)では1つの請求書につき、税率ごとに1回端数処理をするのが原則です。決めた内容は必ず記録に残します(バグか仕様かを見分ける)。
- 型の上限に余裕を持たせる:伸び続ける連番は
BIGINTにしておくと安心です。大きなIDをJavaScriptへ渡すときは、JSONで文字列として渡します。
列の型を変える改修は、テーブルが大きいと時間がかかり、ロックも発生します。本番で行うときは、手順と戻し方を先に用意してください(本番DBマイグレーションの手順)。すでにずれてしまったデータの直し方は、本番データをSQLで安全に直す手順が参考になります。
具体例:「請求書の合計が1円合わない」を4つの手順で追う
社内の受発注システムを一人で保守している担当者の、よくある流れです。
- 以前:経理から「1円合わない」と言われるたびに、管理画面で合計を手で直していた。月に数件、どの注文で起きるかは決まっていない。コードを見ても足し算と掛け算だけで、原因が分からないままだった。
- 4つの手順で追ったあと:①「1円だけ・明細と合計が合わない」で端数処理の型と見当をつけた。②再計算SQLでずれている注文を出すと、すべて「明細が複数行」の注文だった。③区間ごとに値を出すと、画面は明細ごとに税を計算して切り捨ててから足し、帳票は合計してから税を計算して切り捨てていた。たとえば税抜105円の明細が3行あると、画面は 10円 × 3 = 30円、帳票は 315円 × 10% = 31.5円 → 31円で、ちょうど1円ずれる。④経理と相談して「請求書ごと・税率ごとに合計してから切り捨て」にそろえ、丸めを共通の関数1つにまとめた。同じ注文で画面と帳票の合計が一致することを確かめた。
変わったのは、計算の腕ではありません。「どこで丸めているか」を区間ごとに書き出したことです。毎月の手直しも、それきりなくなりました。
影響:数字のずれを追えるようになると、何が変わるか
- 「1円くらい」で流さずに済む:小さなずれの裏にある、丸め方の食い違いを根本から直せます。
- 経理や依頼者に説明しやすくなる:「画面と帳票で、税を計算するタイミングが違っていました」と、具体的に伝えられます。
- ある日突然止まる事故を先回りできる:連番の上限を一度見ておくだけで、「明日から登録できない」を避けられます。
- データを手で直し続けなくて済む:丸めを1か所にまとめれば、同じ手直しを繰り返す時間が消えます(直したつもりが直っていない、を防ぐ)。
明日やること
- 金額を持っている列の型を1つ確かめる。
SHOW COLUMNSで、FLOATやDOUBLEになっていないかを見るだけで大丈夫です。 - よく伸びるテーブルの最大IDを見る。
SELECT MAX(id)を流して、INTの上限(約21億)とくらべてみます。 - コードの中の丸め関数を検索する。
grep -rn "round\|floor\|ceil" src/のように探し、丸めている場所がいくつあるかを数えます。 - 「1円合わない」系の過去の問い合わせを1つ思い出す。①の表のどの型か、当てはめてみます。
今日やるのは、1の「金額の列の型を確かめる」だけでも十分です。INT や DECIMAL なら一安心、FLOAT なら、次の改修の候補が1つ見つかったということです。
数値のずれを追うチェックリスト
- 症状が「端数処理/浮動小数点/表示側の桁あふれ/DBの上限」のどの型かを見たか
- ずれた値と正しい値を並べ、差がいくつかを数字で出したか
- ずれる行に共通する条件(明細の行数・割引・数量など)を探したか
- 入力・計算・保存・表示のどの区間で値が変わったかを書き出したか
- 丸めている関数と、丸めるタイミング(明細ごとか合計か)を確かめたか
- 金額や数量の列の型が、FLOAT / DOUBLE になっていないか確かめたか
- 連番の最大値が、型の上限に近づいていないか確かめたか
- CSVや表計算ソフトで、長い番号や先頭の0が化けていないか確かめたか
- 境目の値(.5・上限ちょうど・16桁以上のID)で、検証環境で再現したか
- 丸め方のルールを決め、記録に残したか
- 直したあと、同じ境目の値で結果が合うことを確かめたか
全部を一度にそろえなくても大丈夫です。まずは「ずれの型を見分ける」と「区間ごとに値を書き出す」の2つだけで、見えなかったずれの場所にぐっと近づけます。
よければ、こちらも
- バグを再現して原因を特定する手順:ずれる行の共通点から、再現の条件を固めるときに。
- バグか仕様かを見分ける:丸め方の「正解」が決まっていないとき、誰とどう決めるかを。
- 本番データをSQLで安全に直す手順:すでにずれてしまった金額や件数を、安全に戻すときに。
- 文字化けの原因を調べる手順:CSVの受け渡しで、数字と一緒に文字も化けているときに。
- テストのないコードを安全に改修する:丸めの関数をまとめる改修を、壊さずに進めるために。

「たった1円なのに」と悩んだ時間は、無駄ではありません。それは、数字を「合っているはず」で済ませずに、ちゃんと確かめようとしたしるしです。数字のずれは、どこかの区間で必ず生まれています。区間を1つずつ見ていけば、その場所に少しずつ近づけます。
まずは明日、金額を入れている列の型を、1つだけ確かめてみてください。
ほかの実務ヒントは記事一覧からどうぞ。保守運用の小さな備えを、メールでも少しずつお届けしています。