# Short Primary Keys
**Check name:** `ShortIntegerPrimaryKeyCheck`
**Lock type:** None (best practice warning)
## Bad
Using SMALLINT or INT for primary keys risks ID exhaustion. SMALLINT maxes out at ~32,767 records, and INT at ~2.1 billion. While 2.1 billion seems large, active applications can exhaust this faster than expected, especially with high-frequency inserts, soft deletes, or partitioned data.
Changing the type later requires an ALTER COLUMN TYPE operation with a full table rewrite and ACCESS EXCLUSIVE lock.
```sql
-- SMALLINT exhausts at ~32K records
CREATE TABLE users (id SMALLINT PRIMARY KEY);
-- INT exhausts at ~2.1B records
CREATE TABLE posts (id INT PRIMARY KEY);
CREATE TABLE events (id INTEGER PRIMARY KEY);
-- Composite PKs with short integers still risky
CREATE TABLE tenant_events (
tenant_id BIGINT,
event_id INT, -- Will exhaust per tenant
PRIMARY KEY (tenant_id, event_id)
);
```
## Good
Use BIGINT for all primary keys to avoid exhaustion:
```sql
-- BIGINT: effectively unlimited (~9.2 quintillion)
CREATE TABLE users (id BIGINT PRIMARY KEY);
-- Identity-backed BIGINT: auto-incrementing and SQL-standard
CREATE TABLE posts (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY);
-- Composite PKs with all BIGINT
CREATE TABLE tenant_events (
tenant_id BIGINT,
event_id BIGINT,
PRIMARY KEY (tenant_id, event_id)
);
```
**Storage overhead:** BIGINT uses 8 bytes vs INT's 4 bytes — only 4 extra bytes per row. For a 1 million row table, this is ~4MB of additional storage, which is negligible compared to the operational cost of changing column types later.
**Identity columns:** On PostgreSQL 10+, prefer `GENERATED ... AS IDENTITY` over `SERIAL`/`BIGSERIAL`. Use `BIGSERIAL` only when you must support older PostgreSQL versions.
**Safe exceptions:** Small, finite lookup tables with <100 entries (e.g., status codes, country lists) can safely use smaller types. Use `safety-assured` to bypass the check for these cases.