Integer Out Of Range Arithmetic Overflow
An UPDATE, INSERT, SELECT, or aggregate that multiplies (or adds) integer values fails with SQLSTATE 22003 and 'integer out of range'. The statement is...
In 10 seconds
A nightly job computes order line totals: UPDATE order_lines SET line_total = unit_price_cents * quantity. It has worked for years, but today it fails with 'ERROR: integer out of range' (SQLSTATE 22003). A handful of large orders multiply out past int4's ceiling of 2,147,483,647 -- for example 2,000,000 cents x 2,000 units = 4,000,000,000. The trap is that PostgreSQL does not silently widen int * int to bigint: the product is computed in int4, overflows, and the int4mul operator raises 22003, rolling back the whole UPDATE. Nothing is written, and the report looks broken because of five rows among thousands.
Why This Happens
PostgreSQL's arithmetic operators are typed: integer * integer resolves to the int4mul function, which returns integer. There is no automatic promotion to bigint based on the magnitude of the result, so the multiplication is performed in 32-bit space and overflows when the true product exceeds 2,147,483,647 -- at which point int4mul raises 22003 rather than wrapping around silently (PostgreSQL checks for overflow, unlike C).
Because the failure happens during expression evaluation inside a single statement, the whole UPDATE rolls back.
The fix has two parts that must both hold: the destination column must be wide enough to store the result (bigint), and the arithmetic must be done in a wide type by casting at least one operand to bigint, so the operator chosen is int8mul (which returns bigint) and never overflows for these magnitudes. The fix has two parts that must both hold: the destination column must be wide enough to store the result (bigint), and the arithmetic must be done in a wide type by casting at least one operand to bigint, so the operator chosen is int8mul (which returns bigint) and never overflows for these magnitudes.
Diagnose
Free Tier -- Basic Approach
Before changing anything, size the problem with one count -- how many rows have a product beyond int4 max. Five. Critically, the check casts to bigint first, so measuring the overflow does not itself overflow.
SELECT count(*) AS overflow_rows FROM order_lines WHERE unit_price_cents::bigint * quantity > 2147483647;
overflow_rows
---------------
5
(1 row)
SELECT count(*) AS overflow_rows FROM order_lines WHERE unit_price_cents::bigint * quantity > 2147483647; -- the ::bigint cast makes the comparison safe; without it the diagnostic would raise the same 22003 it is trying to find.
Fix
Fix both halves: widen the destination and widen the arithmetic. ALTER line_total to bigint (ALTER TABLE), then recompute casting one operand to bigint so the multiplication runs in int8 and cannot overflow (UPDATE 1000). Every total, including the 4,000,000,000 rows, is now computed and stored correctly.
ALTER TABLE order_lines ALTER COLUMN line_total TYPE bigint; UPDATE order_lines SET line_total = unit_price_cents::bigint * quantity;
ALTER TABLE UPDATE 1000
ALTER TABLE order_lines ALTER COLUMN line_total TYPE bigint; then UPDATE order_lines SET line_total = unit_price_cents::bigint * quantity; -- the ::bigint cast selects int8mul (returns bigint), and the widened column stores the result.
Verify
Confirm the column is bigint, the large totals are stored as their true values (not wrapped), and a bare int * int that still overflows is rejected -- proof the fix is the bigint arithmetic and storage, not a relaxed type. information_schema reports bigint, max_total is 4,000,000,000, and SELECT 2000000 * 2000 still raises 22003.
SELECT data_type FROM information_schema.columns WHERE table_name = 'order_lines' AND column_name = 'line_total';
data_type ----------- bigint (1 row)
SELECT max(line_total) AS max_total FROM order_lines;
max_total ------------ 4000000000 (1 row)
SELECT 2000000 * 2000;
ERROR: integer out of range
Read data_type from information_schema.columns (expect bigint), check max(line_total) equals the true 4,000,000,000, and run SELECT 2000000 * 2000 to confirm a literal int * int overflow is still rejected with SQLSTATE 22003.
⏪ Rollback
Nothing to undo from the failed UPDATE -- it rolled back atomically, so line_total is unchanged (still NULL for every row) and no partial or wrapped values were stored. Recovery is forward: measure how many products overflow and how large they really are, widen the destination column to bigint, then recompute with bigint arithmetic. The only data-changing steps are the ALTER (widens the column, rewrites the table) and the recompute UPDATE; both are safe because bigint holds every product seen in the premium evidence with room to spare.
📦 Version Matrix (PG 14--18)
The behavior is identical on PostgreSQL 14 through 18: the int * int product raises 'integer out of range', and widening the column plus bigint arithmetic recomputes all rows cleanly (UPDATE 100). The 22003 overflow contract of int4mul is stable across all supported majors.
| Version | Behavior | Version Notes |
|---|---|---|
PG 14.23 |
broken=integer out of range | ✗ Not available -- use stats_reset age |
PG 15.18 |
broken=integer out of range | ✗ Not available -- use stats_reset age |
PG 16.14 |
broken=integer out of range | ✗ Not available -- use stats_reset age |
PG 17.10 |
broken=integer out of range | ✗ Not available -- use stats_reset age |
PG 18.4 |
broken=integer out of range | ✗ Not available -- use stats_reset age |
⚠ Common Mistakes
Stop this incident before it pages you
The free runbook clears today's failure. Pro adds the standing guardrails -- the alert query, the dashboard, and the managed-service knobs -- so it never becomes an incident again.
- ✓ A ready-to-paste alert query with the right thresholds.
- ✓ The topology change that prevents recurrence.
- ✓ The exact knobs on RDS, Azure Flexible Server & Cloud SQL.
Frequently Asked Questions
Why doesn't PostgreSQL just use bigint when the product is large?
Because operator and result types are resolved from the operand types at plan time, not from runtime magnitude. integer * integer is int4mul, which returns integer; the engine cannot know in advance that a particular row's product will be large. It checks for overflow and raises 22003 rather than wrapping silently. Force bigint by casting an operand.
The error doesn't name the row -- how do I find the offenders?
Recompute in a wider type and compare to the limit: SELECT id, unit_price_cents, quantity, unit_price_cents::bigint * quantity AS true_total FROM order_lines WHERE unit_price_cents::bigint * quantity > 2147483647. The bigint cast makes the diagnostic safe and reveals each true product.
Do I need to change the column type, or just the query?
Both, if you store the result. The expression must be evaluated in bigint (cast an operand) so it does not overflow, AND the destination column must be bigint so it can hold the result. If you are only computing on the fly (a SELECT), casting the operand is enough; if you persist it, widen the column too.
Is bigint enough, or should I use numeric?
bigint holds up to 9.2 x 10^18, which is ample for cents-times-quantity totals like these (the premium check confirms max_product is 4,000,000,000). Use numeric when values can exceed bigint or when you need exact decimal scaling (money with fractional cents, very large accumulations); otherwise bigint is faster and smaller.
How do I keep this from recurring as data grows?
Cast operands to a wide type in the query so the arithmetic never overflows, size stored columns for the largest plausible result, and keep a standing guard -- count rows whose product would exceed the column's capacity -- that must read 0. Review integer columns that feed multiplications during capacity planning.