夜のオフィスで、電卓と請求書の控えを手元に置き、モニターの数字の並んだ表と見比べながら考え込む保守運用の担当者

金額が1円ずれる・数が化ける|丸め誤差と桁あふれの追い方

「請求書の合計と、管理画面の合計が1円合わないんですが……」

経理の方からそう言われて、計算のコードを開いてみる。足し算と掛け算しかしていない。テストでも合っていた。それなのに、ある注文だけ1円ずれる。たった1円なのに、どこから手をつければいいのか分からなくて、画面の前で手が止まる。数字のバグは、そういう静かな困り方をさせるものです。

これは、あなたの計算が雑だったわけではありません。コンピュータの数字には「小数をぴったり表せない」「どこかで丸めている」「入る桁に上限がある」という、ふだんは見えない性質があります。ずれは、その性質がどこかで顔を出しただけのことがほとんどです。

この記事では、金額や件数がずれる・化けるバグを追う順番を、小さく分けて一緒に整理していきます。

結論:数値のバグは、次の4つの順で追います。①ずれの「型」を見分ける(小さなずれ/端数処理の違い/大きな数が化ける・上限で止まる)、②ずれた値を「正しい値」と並べて、差を数字で出す、③値の通り道(入力 → 計算 → 保存 → 表示・出力)のどこでずれたかを区間で切る、④境目の値で再現してから直す。直し方の基本は、金額は小数を使わず「整数(円)」か「10進数の型」で持つ、丸める場所と方法を1か所に決める、型の上限に余裕を持たせるの3つです。

なお、関数の丸め方や型の上限は、言語・DB・バージョンで変わります。ここで持ち帰ってほしいのは順番と観点です。手元の環境の公式ドキュメントで確かめ、必ず検証環境で試してから直してください。

何が起きているか:数字には「見えない3つの性質」がある

数値のずれは、たいてい次の3つのどれかから生まれます。

一つひとつは小さな性質です。ただ、値が画面から入り、計算され、DBに保存され、帳票やCSVで出ていくあいだに、何度もこの性質をくぐります。どこか1か所でずれれば、最後の数字がずれる。それだけのことです。

よくある症状は、次のような形で現れます。

数値のずれを追う4つの手順

左から右へ4つの場面が矢印でつながり、キーボード入力、電卓での計算、データベースの円筒への保存、紙の帳票の出力が並んでいて、3つ目の保存の場面だけ虫眼鏡が当てられている図
値は「入力 → 計算 → 保存 → 表示」の順に通っていく。各区間で値を書き出せば、どこでずれたかを区切って探せる

① ずれの「型」を見分ける

最初に、症状がどの型に当てはまるかを見ます。型が分かると、疑う場所がぐっと絞れます。

症状疑う型まず見る場所
1円・0.01だけずれる、明細と合計が合わない端数処理の違い丸めている関数と、丸めるタイミング(明細ごとか合計か)
19.999999 のような値、== の比較が成り立たない浮動小数点の誤差金額や数量を float / double で持っている箇所
長い番号の末尾が 0 になる、IDが隣の番号に変わる表示・受け渡し側の桁あふれ表計算ソフト、JavaScript、JSONの受け渡し
ある日から登録だけエラーになる、値が最大値で止まるDBの型の上限連番(ID)や集計値を入れている列の型

「1円ずれる」と「番号が化ける」は、同じ“数字のバグ”に見えても、原因の場所がまったく違います。ここを分けておくだけで、遠回りがだいぶ減ります。

② ずれた値を「正しい値」と並べて、差を数字で出す

次に、ずれた結果と、手計算や電卓で出した正しい値を横に並べます。差がいくつかを、数字で出しておくのがポイントです。

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件選び、各区間で値を書き出して、どこから値が変わったかを探します。

区間ごとに値を出すには、計算の前後に一時的にログを仕込むのが確実です(改修時にログを仕込んで原因を追う)。DBの列の型は、次のように確かめられます。

-- 列の型を確かめる(MySQLの例。PostgreSQLなら \d テーブル名)
SHOW COLUMNS FROM orders;

連番の上限が気になるときは、いまの最大値と型の上限を並べて見ます。

-- いまの最大IDを確かめる(INT 符号付きの上限は 2147483647)
SELECT MAX(id) FROM orders;

上限の半分を超えていたら、すぐに困ることはなくても、次の改修の候補としてメモしておく価値があります。

ここで「計算までは合っていたのに、CSVを表計算ソフトで開いたら化けていた」と分かることも、よくあります。その場合、プログラムは悪くありません。長い番号を数値として読み込んで、15桁を超えた部分が 0 に丸められたり、先頭の 0 が消えたりしているだけです。番号は文字列として扱うよう、出力側か開き方を変えれば済みます。

④ 境目の値で再現してから直す

ずれる区間が分かったら、境目になる値を使って、検証環境で再現します。

再現できたら、直し方を決めます。基本は次の3つです。

列の型を変える改修は、テーブルが大きいと時間がかかり、ロックも発生します。本番で行うときは、手順と戻し方を先に用意してください(本番DBマイグレーションの手順)。すでにずれてしまったデータの直し方は、本番データをSQLで安全に直す手順が参考になります。

具体例:「請求書の合計が1円合わない」を4つの手順で追う

社内の受発注システムを一人で保守している担当者の、よくある流れです。

変わったのは、計算の腕ではありません。「どこで丸めているか」を区間ごとに書き出したことです。毎月の手直しも、それきりなくなりました。

影響:数字のずれを追えるようになると、何が変わるか

明日やること

  1. 金額を持っている列の型を1つ確かめる。SHOW COLUMNS で、FLOAT や DOUBLE になっていないかを見るだけで大丈夫です。
  2. よく伸びるテーブルの最大IDを見る。SELECT MAX(id) を流して、INT の上限(約21億)とくらべてみます。
  3. コードの中の丸め関数を検索する。grep -rn "round\|floor\|ceil" src/ のように探し、丸めている場所がいくつあるかを数えます。
  4. 「1円合わない」系の過去の問い合わせを1つ思い出す。①の表のどの型か、当てはめてみます。

今日やるのは、1の「金額の列の型を確かめる」だけでも十分です。INT や DECIMAL なら一安心、FLOAT なら、次の改修の候補が1つ見つかったということです。

数値のずれを追うチェックリスト

全部を一度にそろえなくても大丈夫です。まずは「ずれの型を見分ける」と「区間ごとに値を書き出す」の2つだけで、見えなかったずれの場所にぐっと近づけます。

よければ、こちらも

明るい朝のオフィスで、紙の帳票とモニターの表を見比べて、ぴったり合ったことにほっとした笑顔を浮かべる保守運用の担当者
どこで丸めているかが分かれば、小さなずれは、もう怖くない

「たった1円なのに」と悩んだ時間は、無駄ではありません。それは、数字を「合っているはず」で済ませずに、ちゃんと確かめようとしたしるしです。数字のずれは、どこかの区間で必ず生まれています。区間を1つずつ見ていけば、その場所に少しずつ近づけます。

まずは明日、金額を入れている列の型を、1つだけ確かめてみてください。

ほかの実務ヒントは記事一覧からどうぞ。保守運用の小さな備えを、メールでも少しずつお届けしています。

関連用語