【SQL】ゼロ除算エラーと回避方法を考えてみよう(業務視点)
油断大敵!
今回は、システム開発の現場では油断すると発生しやすいと思われる不具合『ゼロ除算エラー』の回避方法について、考えてみたいと思います。
ゼロ除算とは?
システム上で除算をする際に分母となる項目にゼロが存在するため、計算できずにエラーを返してしまう事象です。
実装しているプラットフォームによって、返すエラーは異なります。
たとえば下記のようなものです。
- ゼロ除算エラー
- 浮動小数点例外
- エラーを返さずダウン
いずれにしても処理が続行できず、停止は免れないでしょう。
試してみよう!
「今ひとつ、イメージが沸かない」「普通は起きないでしょう」そう思う方もいるでしょう。
では、今回はSQLで実際にゼロ除算エラーを作って、その回避方法も試してみましょう。
具体例をPostgreSQLで書きますので、よろしければ試してみてください。
※簡単に試す方法はこちらの記事を参考にして下さい。
サンプル(販売業)
たとえば、お客様に商品を販売した売上を記録してみます。
ここではシンプルに、お会計の合計金額と品物の個数だけを記録します。
WITH 売上 AS MATERIALIZED(
SELECT 2000 AS "合計金額" ,5 AS "個数"
UNION ALL
SELECT 1000 ,0
)
SELECT * FROM 売上;
実行結果は、2件の売上データが表示されています。
エラーが発生!
では、単価を求めてみましょう。
計算方法は『合計金額 ÷ 個数』とします。
WITH 売上 AS MATERIALIZED(
SELECT 2000 AS "合計金額" ,5 AS "個数"
UNION ALL
SELECT 1000 ,0
)
SELECT *
, "合計金額" / "個数" AS "単価" -- 単価を求める除算を追加!
FROM 売上;
英語ですが、ゼロ除算エラーが発生しました!
原因は、売上データ2行目の数量が0個であり、合計金額の千円を0で割ろうとしたためです。
1000 / 0 = Error
回避してみよう!
では、エラーが発生しないように回避策を実装します。
SQLでは、まず思い付くものではCASE句で条件を設定する方法があります。
WITH 売上 AS MATERIALIZED(
SELECT 2000 AS "合計金額" ,5 AS "個数"
UNION ALL
SELECT 1000 ,0
)
SELECT *
, CASE WHEN "個数" = 0 THEN 0 -- 個数が0個の場合は、単価も0円にする
ELSE "合計金額" / "個数" -- 個数が0個以外の場合は、除算する
END AS "単価" -- 単価を求める除算
FROM 売上;
エラーは発生せず、無事に結果が表示されました。
ちょっと待った!
これで「めでたし、めでたし」ではありません。
もしかしたら、こう言われることもあり得ます。
「単価をゼロにして良いって、誰が言いましたか?」
先程の対策は、インシデント対応としては良かったのかもしれません。
(業務が止まらないことを最優先とする場合)
とはいえ、実は詰めが甘かったのです。
業務で『求められていること』は?
ゼロでの除算は『計算ができない』からエラーになりました。
しかし、CASE句では計算できないものに対して計算結果を出しました。
そうです、単価にゼロを設定しました。
計算できないのに「ゼロを設定しよう」と判断したのです。
意外なことに、これは悪い意味での『慣れ』が影響することがあります。
「このエラー、前にも別の開発で見たことある! その時は・・・」
と、経験に基づいて決定してしまうこともあるでしょう。
しかし、要件定義や設計段階で潰せなかったイレギュラーが発生した場合は、それをどのように対応するかは、ステークホルダと合意が取れた状態で対応する必要があります。
もし、ステークホルダと「単価もゼロにしてください」との合意が取れていれば、先程の対策でOKです。
しかし他にも、たとえば下記のような要件が想定されます。
- 「合計金額を、そのまま単価にしてください」
- 「単価は設定しないでください」
では、これらの要件をひとつずつ試してみましょう。
合計金額を単価にしてみよう
個数がゼロの場合に、合計金額を設定します。
修正箇所はTHENの後ろです。
WITH 売上 AS MATERIALIZED(
SELECT 2000 AS "合計金額" ,5 AS "個数"
UNION ALL
SELECT 1000 ,0
)
SELECT *
, CASE WHEN "個数" = 0 THEN "合計金額" -- 個数が0個の場合は、合計金額にする
ELSE "合計金額" / "個数" -- 個数が0個以外の場合は、除算する
END AS "単価" -- 単価を求める除算
FROM 売上;
2行目の数量ゼロのレコードが『単価=合計金額』になりましたね。
単価を「設定しない」とは?
この要件、すぐに意図が読み取れたでしょうか?
「え?設定しない?それって、無いのだから結局ゼロで良いんじゃないの?」
と、思った方もいらっしゃるのではと思います。
これは、本当に単価が0円のものが存在する場合が考えられます。
つまり『計算出来ないデータ』と『本物の単価0円』を区別するためです。
では、何を設定するのか試してみましょう。
WITH 売上 AS MATERIALIZED(
SELECT 2000 AS "合計金額" ,5 AS "個数"
UNION ALL
SELECT 1000 ,0
)
SELECT *
, CASE WHEN "個数" = 0 THEN CAST(NULL AS NUMERIC)
-- 個数が0個の場合は、NULLにする
-- ※一応、型変換しておいた方が無難と思われる
ELSE "合計金額" / "個数" -- 個数が0個以外の場合は、除算する
END AS "単価" -- 単価を求める除算
FROM 売上;
こうなります。
「ああ~!なるほど・・・」と思った方もいらっしゃるのではないでしょうか?
設定しないという要件通り『設定しなかった』つまり『無(null)』です。
さいごに
今回は、ゼロ除算エラーという技術的なテーマを紹介しつつ、後半では要件に沿った判断が重要であることを紹介しました。
もし、不具合が発生し何かしらの対応(改修など) を実施する際は、まず上長に相談しステークホルダとの合意を取りましょう!
※よほど緊急なインシデント対応では、まず業務が停止しないことを優先する必要があるので、この限りではありませんが;
では、今回は以上です。
ここまでお付き合い頂き、ありがとうございましたm(_ _)m
Discussion