何が起きているか
除算を含むクエリを実行したところ、PostgreSQLが以下のエラーをスローしました:
ERROR: division by zero
実行時に分母がゼロと評価されました。PostgreSQLは即座に停止します — 部分的な結果も警告もなく、ただハードエラーのみです。
よくある原因:
- パーセンテージや比率の列(
sales / total_sales * 100) - グループが空の場合の手動平均(
SUM(x) / COUNT(y)) - 一部のグループのレコード数がゼロになる集計クエリ
- 疎なデータに対するユーザーあたり収益などのKPIクエリ
エラーを再現する
最もシンプルなケース:
SELECT 10 / 0;
-- ERROR: division by zero
実際のテーブルでの例:
SELECT
department,
total_revenue / headcount AS revenue_per_person
FROM department_stats;
-- headcount = 0 の部署が存在するまでは正常動作する
このクエリはheadcount = 0の行に実際に当たったときのみ失敗します。テストデータにゼロが含まれることはほとんどないため、開発環境ではなく本番環境で最初に発覚するのはまさにそのためです。
デバッグ:問題のある行を特定する
クエリを修正する前に、状況を把握しましょう:
-- 分母がゼロの行はどれか?
SELECT department, headcount
FROM department_stats
WHERE headcount = 0;
-- 集計の場合:ソーステーブルで空のグループを確認する
SELECT department, COUNT(*) AS row_count
FROM sales
GROUP BY department;
2行なのか2,000行なのか?その数によって、適切な修正方法が変わります。
修正1:NULLIF — 最も手軽な修正
NULLIF(a, b)はa = bのときNULLを返し、それ以外の場合はaを返します。分母をラップします:
SELECT
department,
total_revenue / NULLIF(headcount, 0) AS revenue_per_person
FROM department_stats;
headcountが0の場合、除算はtotal_revenue / NULLとなり、NULLが返されます — エラーにはなりません。その行は引き続き結果に表示されますが、該当列の値はNULLになります。
NULLではなく0を返したい場合はCOALESCEを追加します:
SELECT
department,
COALESCE(total_revenue / NULLIF(headcount, 0), 0) AS revenue_per_person
FROM department_stats;
修正2:CASE WHEN — より明示的な制御
フォールバックがシンプルでない場合、CASE WHENで完全に制御できます:
SELECT
department,
CASE
WHEN headcount = 0 THEN NULL
ELSE total_revenue / headcount
END AS revenue_per_person
FROM department_stats;
またはアプリケーションが検出できるセンチネル値を返す方法もあります:
SELECT
department,
CASE
WHEN headcount = 0 THEN -1 -- 呼び出し元に「データなし」を伝えるシグナル
ELSE total_revenue / headcount
END AS revenue_per_person
FROM department_stats;
CASE WHENは短絡評価を行います — PostgreSQLは条件を先にチェックし、ガード条件が一致した場合は除算を試みません。ビジネスロジックが絡む場合、NULLIF/COALESCEのネストよりも若干読みやすくなります。
修正3:集計クエリ
集計でのゼロ除算は、通常、空のグループが原因です:
-- 行が存在しないカテゴリでCOUNT = 0 → エラー発生
SELECT
category,
SUM(amount) / COUNT(order_id) AS avg_order_value
FROM orders
GROUP BY category;
COUNTをNULLIFでラップします:
SELECT
category,
SUM(amount) / NULLIF(COUNT(order_id), 0) AS avg_order_value
FROM orders
GROUP BY category;
さらに良い方法として — 平均を求めるならAVG()を使いましょう。空のセットに対して自動的にNULLを返すため、追加のラップは不要です:
-- AVG() はゼロ除算エラーをスローしない
SELECT category, AVG(amount) AS avg_order_value
FROM orders
GROUP BY category;
修正4:パーセンテージ計算
このエラーが最も頻繁に現れるのはパーセンテージクエリです。典型的なパターン:
-- リスクあり:整数除算 + 合計がゼロになる可能性
SELECT
status,
COUNT(*) * 100 / total AS pct
FROM orders
CROSS JOIN (SELECT COUNT(*) AS total FROM orders) t
GROUP BY status, total;
安全なバージョン:
SELECT
status,
ROUND(
COUNT(*) * 100.0 / NULLIF(total, 0),
2
) AS pct
FROM orders
CROSS JOIN (SELECT COUNT(*) AS total FROM orders) t
GROUP BY status, total;
二つの修正を一度に適用しています:NULLIFでゼロを防ぎ、100(整数)の代わりに100.0(浮動小数点)を使用することで除算の切り捨てを防ぎます。33.33のはずが33が返るクエリも、それはそれでバグです。
修正5:テーブルレベルでの制約
headcountのような列が決してゼロにならないべきなら、スキーマでそれを明示します:
ALTER TABLE department_stats
ADD CONSTRAINT headcount_positive CHECK (headcount > 0);
不正なデータが入らなくなるため、クエリがゼロのケースに当たることもなくなります。NOT NULLと組み合わせましょう:
ALTER TABLE department_stats
ALTER COLUMN headcount SET NOT NULL,
ADD CONSTRAINT headcount_positive CHECK (headcount > 0);
これが最も恒久的な修正ですが、ゼロが本当に無効なデータである場合にのみ適用できます。正当な空の状態を表すゼロには使えません。
修正を確認する
リテラルを使った簡単なサニティチェック:
-- エラーではなくNULLが返るべき
SELECT 100 / NULLIF(0, 0);
-- 結果: NULL
-- COALESCEフォールバックを使った場合
SELECT COALESCE(100 / NULLIF(0, 0), 0);
-- 結果: 0
CTEを使ってゼロ行を注入し、実際のテーブルでテストします:
WITH test_data AS (
SELECT 'Engineering' AS dept, 0 AS headcount, 50000 AS revenue
)
SELECT
dept,
revenue / NULLIF(headcount, 0) AS rev_per_person
FROM test_data;
-- 期待結果: Engineering | NULL (エラーなし)
次に、元の失敗したクエリを実行します。これで問題なく完了するはずです。
クイックリファレンス
- 最もシンプルな修正:
/ NULLIF(denominator, 0) - NULLの代わりに0を返す:
COALESCE(x / NULLIF(y, 0), 0) - 複雑なロジック:
CASE WHEN y = 0 THEN ... ELSE x / y END - 平均値:
AVG()を使う — 空のセットをネイティブに処理する - ソースで防ぐ:
CHECK (column > 0)制約を追加する
教訓
十中八九、根本原因はデータ品質にあります。正の値を保持するはずだった列が、制約がなかったためにゼロを黙って受け入れてしまったのです。NULLIFはクエリ内の症状を修正しますが — そのゼロが正当な空の状態なのか、データパイプラインのバグなのかを遡って確認する価値はあります。
疎なデータが通常であるレポートやダッシュボードには、NULLIFが適切なデフォルトです。ゼロの分母が破損したデータを示すトランザクションクエリには、テーブルレベルでCHECK制約を適用し、上流のソースを修正しましょう。

