How to Fix PostgreSQL 'division by zero' Error in Aggregate Queries
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: An aggregate expression (e.g.,
SUM(x) / COUNT(y)or a ratio computation) hit a zero denominator at runtime, causing PostgreSQL to throw a fataldivision by zeroand abort the entire query. - How to fix it: Wrap every denominator in
NULLIF(denominator, 0)so division by zero returnsNULLinstead of crashing, then useCOALESCE()to substitute a safe default. - Shortcut: Use our Client-Side Sandbox above to paste your failing query and auto-refactor it — secrets never leave your browser.
The Incident (What Does the Error Mean?)
Raw error output from psql / application logs:
ERROR: division by zero
CONTEXT: SQL function "your_function" during inlining
or inline in a query runner:
ERROR: division by zero
LINE 1: SELECT department, SUM(revenue) / COUNT(orders) AS avg_rev ...
Immediate consequence: PostgreSQL aborts the entire transaction — not just the offending row. Every row in the result set is discarded. If this query is inside a larger transaction block, all preceding DML in that block is also rolled back. In application terms: your API endpoint returns a 500, your BI dashboard silently fails to load, or your ETL pipeline halts mid-run with no partial output.
The root cause is deterministic: at least one group (or the entire dataset) produced a denominator of exactly 0 — either because COUNT(orders) returned zero for an empty group, or because a SUM/expression resolved to zero for a specific cohort.
The Attack Vector / Blast Radius
This is not a rare edge case — it is a guaranteed production failure in any system that:
- Computes ratios over user-segmented or time-windowed data (where some windows will always be empty)
- Runs month-over-month or week-over-week growth calculations (
(current - prior) / prior) wherepriorcan be zero for new cohorts - Uses dynamic
GROUP BYover sparse datasets (e.g., per-region, per-product-SKU) - Executes inside stored procedures or views called by ORMs — the ORM has no visibility into this failure until it propagates as an unhandled exception
Blast radius in production:
| Layer | Impact |
|---|---|
| Application API | HTTP 500, user-facing error |
| ORM / Connection Pool | Transaction aborted, connection returned dirty |
| ETL / Data Pipeline | Job failure, no partial output, potential data gap |
| Materialized Views | Refresh fails silently or with error, stale data served |
| Stored Procedures | Entire procedure stack unwinds, calling code must handle |
The failure is data-dependent, meaning it passes all your staging tests (where data is clean and balanced) and detonates in production on the first sparse cohort.
How to Fix It (The Solution)
Basic Fix — NULLIF() Guard on the Denominator
The pattern is universal: never divide directly. Always guard with NULLIF(denominator, 0).
-- Failing query: crashes when COUNT(orders) = 0 for any group
- SELECT
- department,
- SUM(revenue) / COUNT(orders) AS avg_revenue_per_order
- FROM sales
- GROUP BY department;
+ SELECT
+ department,
+ SUM(revenue) / NULLIF(COUNT(orders), 0) AS avg_revenue_per_order
+ FROM sales
+ GROUP BY department;
NULLIF(x, 0) returns NULL when x = 0, converting a fatal crash into a NULL result. Downstream, NULL is semantically correct — it means "undefined" for that group, not zero.
Enterprise Best Practice — COALESCE + NULLIF + Defensive Casting
For production-grade queries, especially those feeding dashboards or SLAs, combine NULLIF with COALESCE to substitute an explicit default and add FILTER clauses to make the intent auditable:
-- Anti-pattern: multiple unguarded divisions in a window function context
- SELECT
- user_id,
- date_trunc('month', created_at) AS month,
- SUM(revenue) / SUM(prior_revenue) AS mom_growth,
- SUM(clicks) / SUM(impressions) * 100.0 AS ctr_pct
- FROM metrics
- GROUP BY 1, 2;
+ SELECT
+ user_id,
+ date_trunc('month', created_at) AS month,
+ -- NULLIF guards zero denominator; COALESCE replaces NULL with 0 for downstream consumers
+ COALESCE(
+ SUM(revenue)::NUMERIC / NULLIF(SUM(prior_revenue), 0),
+ 0
+ ) AS mom_growth,
+ -- Explicit NUMERIC cast prevents integer division truncation AND zero-division
+ COALESCE(
+ (SUM(clicks)::NUMERIC / NULLIF(SUM(impressions), 0)) * 100.0,
+ 0.0
+ ) AS ctr_pct
+ FROM metrics
+ GROUP BY 1, 2;
For window functions — same pattern applies:
- SELECT
- order_id,
- revenue / SUM(revenue) OVER (PARTITION BY region) AS revenue_share
- FROM orders;
+ SELECT
+ order_id,
+ revenue / NULLIF(SUM(revenue) OVER (PARTITION BY region), 0) AS revenue_share
+ FROM orders;
For stored procedures / PL/pgSQL, add an explicit guard block:
- result := numerator_val / denominator_val;
+ IF denominator_val = 0 THEN
+ result := NULL; -- or 0, depending on business logic
+ ELSE
+ result := numerator_val / denominator_val;
+ END IF;
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This class of error must be caught before it reaches production. Three enforcement layers:
1. Static SQL Linting with sqlfluff
Add a custom sqlfluff rule or use the L006/L054 rule families. For teams using sqlfluff in CI:
# .github/workflows/sql-lint.yml
- name: Lint SQL for unsafe division
run: |
sqlfluff lint ./sql --dialect postgres --rules L006
# Add a grep-based hard fail for unguarded division patterns
grep -rn '/ COUNT\|/ SUM\|/ total\|/ sum' ./sql && \
echo 'FAIL: Unguarded division detected. Wrap denominator in NULLIF()' && exit 1 || true
2. pgTAP Unit Tests for Sparse Data Scenarios
Every ratio-computing query must have a pgTAP test that injects a zero-denominator group:
-- pgTAP test: verify no division by zero on empty cohort
SELECT ok(
(SELECT avg_revenue_per_order FROM v_department_metrics WHERE department = 'EMPTY_DEPT') IS NULL,
'Empty department returns NULL, not error'
);
3. OPA / Policy-as-Code for dbt Models
If your stack uses dbt, enforce a custom generic test:
# models/schema.yml
models:
- name: department_metrics
columns:
- name: avg_revenue_per_order
tests:
- not_division_by_zero # custom dbt test macro
-- macros/test_not_division_by_zero.sql
{% macro test_not_division_by_zero(model, column_name) %}
SELECT COUNT(*)
FROM {{ model }}
WHERE {{ column_name }} IS NULL
AND _denominator_col = 0 -- adapt to your model
{% endmacro %}
4. Runtime Guard via PostgreSQL CHECK Constraint on Views
For materialized views that refresh on a schedule, wrap the definition itself:
CREATE MATERIALIZED VIEW mv_kpi_ratios AS
SELECT
segment,
COALESCE(SUM(revenue) / NULLIF(SUM(sessions), 0), 0) AS revenue_per_session
FROM events
GROUP BY segment;
-- Refresh will now never fail on zero-session segments
Bottom line: NULLIF(denominator, 0) costs you zero performance. There is no valid reason to ship a ratio computation without it.