MySQL UNSIGNED Integer → PostgreSQL Migration

Why PostgreSQL has no unsigned integer types, and how to pick a signed type that covers the same value range without silent truncation.

BLOCKER

Quick answer

PostgreSQL has no unsigned integer family at all. Move to the next-larger signed type that comfortably covers your real maximum value, or add a CHECK (col >= 0) constraint if the current type is already large enough.

Why it breaks

A MySQL "INT UNSIGNED" column stores 0 to ~4.29 billion; PostgreSQL's INTEGER stores roughly -2.1 billion to +2.1 billion — only about half the positive range. Renaming the type one-for-one silently caps every value above ~2.1 billion, which fails on insert or, worse, wraps/overflows depending on how the load tool handles it.

Real examples

Bumping to the next-larger signed type

MySQL
CREATE TABLE users (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);
PostgreSQL
CREATE TABLE users (
  id BIGINT GENERATED ALWAYS AS IDENTITY,
  PRIMARY KEY (id)
);
-- INT UNSIGNED (max ~4.29B) needs BIGINT (max ~9.2 quintillion), not INTEGER (max ~2.1B)
A same-named INTEGER column would silently cap this table at roughly half of what the original MySQL column could hold.

CHECK constraint when the type already has headroom

MySQL
CREATE TABLE inventory (
  quantity SMALLINT UNSIGNED NOT NULL
);
PostgreSQL
CREATE TABLE inventory (
  quantity INTEGER NOT NULL CHECK (quantity >= 0)
);
SMALLINT UNSIGNED's max (65,535) fits easily inside a signed INTEGER, so a CHECK constraint is enough to preserve the non-negative rule without over-provisioning storage.

Safe migration options

  • Bump to the next-larger signed type: TINYINT UNSIGNED → SMALLINT, SMALLINT UNSIGNED → INTEGER, INT UNSIGNED → BIGINT, BIGINT UNSIGNED → NUMERIC(20,0) (no signed 64-bit type covers the full unsigned BIGINT range).
  • If the column is already comfortably within range for the smaller signed type, keep the type and add CHECK (column_name >= 0) to preserve the non-negative constraint.
  • For a primary/foreign key specifically, prefer the type bump over the CHECK constraint — join performance depends on matching types on both sides.

PostgreSQL solution

There is no PostgreSQL syntax equivalent to UNSIGNED. The fix is a type decision made per column based on the actual data range, not a keyword swap.

Validation

Before cutover

Before cutover, run: SELECT MAX(col) FROM table; against the source MySQL table and confirm the chosen PostgreSQL type's max value comfortably exceeds it — don't just check the current max, budget headroom for growth.

Scan your own schema

This page documents detector rule UNSIGNED in the DevEquiv analyzer — paste or upload your real MySQL/MariaDB schema to find every occurrence of this (and 19 other documented issues) automatically, with a prioritized readiness report.

Analyze a schema free

References

  • MySQL Manual — Integer Types (v8.0) — checked 2026-07-30
  • PostgreSQL Manual — Numeric Types (v17) — checked 2026-07-30

Last verified 2026-07-30.