Cookbook/ Data Types & Coercion/ Integer Out Of Range Arithmetic Overflow
Data Types & Coercion · Runbook

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...

🧩 5 steps ⏱ ~15 min 📊 Intermediate 🐘 PostgreSQL 14--18 Last reviewed June 26, 2026

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

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.

SQL
SELECT count(*) AS overflow_rows
FROM order_lines
WHERE unit_price_cents::bigint * quantity > 2147483647;
OUTPUT -- captured on real PostgreSQL
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.

This is a Pro lesson

Get every Learning Pathway and cookbook recipe -- grounded in PostgreSQL source code, with diagnostics, fixes, and prevention for each topic.

Continue this lesson to learn:

  • Advanced Approach -- Production-Safe
  • All 36 Learning Pathway lessons
  • 170+ cookbook recipes
  • Source-grounded diagnostics & fixes

Secure checkout Cancel anytime Source-grounded

FIX

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.

SQL
ALTER TABLE order_lines ALTER COLUMN line_total TYPE bigint;
UPDATE order_lines SET line_total = unit_price_cents::bigint * quantity;
OUTPUT -- captured on real PostgreSQL
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

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.

SQL
SELECT data_type FROM information_schema.columns
WHERE table_name = 'order_lines' AND column_name = 'line_total';
OUTPUT -- captured on real PostgreSQL
data_type
-----------
 bigint
(1 row)
SQL
SELECT max(line_total) AS max_total FROM order_lines;
OUTPUT -- captured on real PostgreSQL
max_total
------------
 4000000000
(1 row)
SQL
SELECT 2000000 * 2000;
OUTPUT -- captured on real PostgreSQL
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

Assuming int * int widens to bigint automatically because the result 'obviously' needs it. It does not -- the operator is chosen from the operand types, not the result magnitude, so the product is int4 and overflows. Cast an operand: unit_price_cents::bigint * quantity.
Widening only the column and not the arithmetic. ALTER COLUMN line_total TYPE bigint alone does not help if the expression is still int * int -- the overflow happens during evaluation, before the value is assigned to the wider column.
Diagnosing the problem with the same overflowing expression. A query like WHERE unit_price_cents * quantity > 2147483647 overflows while trying to find overflows; cast to bigint inside the check so the diagnostic itself is safe.
Trusting that the old data is fine because the job 'used to work'. The values that overflow only appeared as orders grew; once a product crosses 2^31-1 the job fails, and any earlier silently-stored result computed differently should be re-validated.
Forgetting addition and aggregates. SUM(integer) already returns bigint and will not overflow, but a + b between two int columns uses int4pl and can; and SUM(bigint) returns numeric. Match the type to the largest value the expression can produce.
🔒 Pro · Prevent

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.
Unlock Pro$24.99/mo · $199/yr

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.

References

Keep going

Related & next steps