Auto-increment integers are the simplest primary key for a single-database application. UUIDs are the correct default for anything distributed. UUID v7 has largely closed the performance gap that made UUID v4 a painful choice for high-write tables. This article works through the trade-offs systematically.

What auto-increment gives you

Auto-increment (SERIAL in PostgreSQL, AUTO_INCREMENT in MySQL, AUTOINCREMENT in SQLite) generates a sequential integer from a central counter. The benefits are concrete:

The limitation is that the counter is centralised. Two databases cannot independently generate unique sequential IDs without coordination.

What UUID v4 costs

UUID v4 trades away all of the above:

The benefit is complete independence: any node, offline client, or edge function can generate a globally unique ID without coordination.

What UUID v7 changes

UUID v7 keeps the independence of UUIDs but recovers the sequential insert property. Its 48-bit timestamp prefix means consecutive inserts arrive in order, so:

The remaining costs: UUID v7 is still 16 bytes versus 4–8 for an integer, and it still looks opaque in logs. But the index fragmentation problem — the main practical argument against UUID primary keys — is gone for typical insert patterns.

Direct comparison

PropertyAuto-incrementUUID v4UUID v7
Storage per key4–8 bytes16 bytes16 bytes
B-tree insert patternSequentialRandomSequential (ms)
Requires coordinationYes (central DB)NoNo
Works offline / distributedNoYesYes
Creation time in IDNoNoYes
Safe in public URLsNo — enumerableYesNo — timestamp leaks
Human readableYesNoNo

When to use auto-increment

When to use UUID v7

When to keep UUID v4

The enumeration risk of auto-increment

Auto-increment IDs are enumerable: anyone who sees invoice_id=1042 knows there are at least 1041 other invoices and can likely request them sequentially. This is a significant concern for any ID that appears in a URL or API response.

The standard mitigations are: never expose internal IDs in URLs (use a separate public slug or token), or use UUIDs for public-facing identifiers even if you use auto-increment internally. Many applications use both — an internal BIGINT primary key for join efficiency and a UUID column for the external identifier.

PostgreSQL setup in 2026

-- Auto-increment (traditional)
CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- UUID v7 (PostgreSQL 18)
CREATE TABLE orders (
  id UUID DEFAULT uuidv7() PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Dual-key pattern: integer PK + UUID public identifier
CREATE TABLE orders (
  id         BIGSERIAL PRIMARY KEY,
  public_id  UUID DEFAULT uuidv7() UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

The dual-key pattern gives you optimal join performance (integer FK) and safe public identifiers (UUID) at the cost of storing two keys per row.

Further reading

Frequently asked questions

Are UUID primary keys slower than integer primary keys?

UUID v4 keys are slower at scale due to random B-tree inserts causing page splits. UUID v7 largely closes this gap with its timestamp prefix — consecutive inserts land near the rightmost leaf page, similar to auto-increment. The remaining difference is storage size: 16 bytes vs 4–8 bytes for an integer, which affects index depth at very large table sizes.

Do UUIDs hurt database index performance?

UUID v4 does — its randomness causes every insert to land at a different leaf page, leading to page splits and poor cache locality. UUID v7 embeds a millisecond timestamp so consecutive inserts cluster together, behaving like an auto-increment integer for B-tree purposes.

Does PostgreSQL 18 support UUID v7 natively?

Yes. PostgreSQL 18 ships a built-in uuidv7() function that generates RFC 9562-compliant UUID v7 values directly in SQL — no extensions or application-level generation required. The values store in the existing uuid column type unchanged.

Are UUID primary keys slower than integers?

UUID v4 primary keys are measurably slower at scale because random values scatter inserts across the B-tree index, causing page splits. UUID v7 largely closes this gap — its timestamp prefix means consecutive inserts land near the rightmost leaf page, similar to auto-increment. The remaining difference is storage size (16 bytes vs 4–8 bytes for an integer), which affects index depth and cache efficiency at very large table sizes.

Should I use UUID or auto-increment for a new project in 2026?

For a single-database application with no need to merge records from multiple sources, auto-increment is simpler and has marginally better index performance. For any application that may distribute data across multiple databases, sync between devices, expose IDs in public URLs, or needs to generate IDs offline (in mobile apps or edge functions), UUID v7 is the correct default. PostgreSQL 18's native uuidv7() function removes the last friction point for UUID v7 adoption.