Why UUIDs Beat Integers as Primary Keys (And Why Performance Isn’t the Issue)

One of the first schema decisions you face when designing a database table is: Should I use an INT or a UUID as the primary key?

Most developers default to an auto-incrementing integer. It’s simple, compact, and familiar. But UUIDs (a.k.a. GUIDs) are increasingly popular — and many of the old performance objections don’t hold up with modern hardware.

Let’s break this down like developers, not DB theorists.

What’s a UUID, Really?

At the end of the day, a UUID is just a number. Specifically: a 128-bit number.

Compare that to the types you already know:

Type Size (bits) Size (bytes) Max Values
INT 32 4 ~4.3B
BIGINT 64 8 ~9.2 quintillion
UUID 128 16 ~3.4 × 10³⁸

That’s it. No magic. Just more space.

Why Developers Like UUIDs

1. Generate Anywhere

With integers, the database usually assigns the ID:

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50)
);

You don’t know the ID until after the row is inserted. With UUIDs, you can generate the ID in your application:

CREATE TABLE users (
  id UUID PRIMARY KEY,
  name VARCHAR(50)
);

Then in code:

import uuid
user_id = uuid.uuid4()

No database round trip just to get a key.

2. Merge Without Collisions

If you’ve ever tried merging data from two databases that both used AUTO_INCREMENT, you know the pain of ID collisions.

-- From DB A
id | name
---+-------
 1 | Alice
 2 | Bob

-- From DB B
id | name
---+-------
 1 | Charlie
 2 | Dana

Good luck merging those.

With UUIDs:

id                                   | name
-------------------------------------+-------
550e8400-e29b-41d4-a716-446655440000 | Alice
ddeb27fb-d9a0-4624-be4d-4615062daed4 | Bob
d8f1f8d4-4b0f-11ee-be56-0242ac120002 | Charlie
42c9aeea-4b0f-11ee-be56-0242ac120002 | Dana

No collisions. Merge away.

3. Safer URLs

Ever seen a URL like /user/12345? Anyone can guess /user/12346.

UUIDs make this harder:

/user/550e8400-e29b-41d4-a716-446655440000

Much less guessable.

The Performance Myth

Here’s the common objection:

“But UUIDs are so much bigger than integers. Won’t that kill performance?”

Not really.

That’s 12 extra bytes per row compared to an INT. For perspective, a single VARCHAR(255) column can take 255 bytes — orders of magnitude more than the difference between keys.

Modern CPUs handle 128-bit comparisons natively. Databases already juggle kilobytes of row data per query. The cost of comparing two UUIDs instead of two integers is microscopic compared to disk I/O, joins, or network latency.

Real-world bottlenecks are almost never the 16-byte key.

Things to Watch Out For

That said, UUIDs aren’t free candy:

Rule of Thumb for Developers

Benchmark: INT vs UUID in PostgreSQL

Let’s test with two identical tables:

-- INT-based table
CREATE TABLE users_int (
  id SERIAL PRIMARY KEY,
  name TEXT
);

-- UUID-based table
CREATE TABLE users_uuid (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT
);

We’ll insert 1 million rows into each:

-- Insert into INT table
INSERT INTO users_int (name)
SELECT 'user_' || g
FROM generate_series(1, 1000000) g;

-- Insert into UUID table
INSERT INTO users_uuid (name)
SELECT 'user_' || g
FROM generate_series(1, 1000000) g;

Query Performance

Point lookup by primary key:

EXPLAIN ANALYZE
SELECT * FROM users_int WHERE id = 500000;

Output (abridged):

Index Scan using users_int_pkey on users_int
  (cost=0.42..8.44 rows=1 width=36)
  (actual time=0.018 ms)

Now with UUID:

EXPLAIN ANALYZE
SELECT * FROM users_uuid
WHERE id = '550e8400-e29b-41d4-a716-446655440000';

Output (abridged):

Index Scan using users_uuid_pkey on users_uuid
  (cost=0.42..8.44 rows=1 width=52)
  (actual time=0.019 ms)

The difference? Basically zero. Both queries run in ~0.02ms.

Index Size

Let’s compare index sizes:

SELECT
  relname AS index_name,
  pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_all_indexes
WHERE relname LIKE 'users_%_pkey';

Sample output:

index_name index_size
users_int_pkey 35 MB
users_uuid_pkey 70 MB

Yes, the UUID index is bigger (about double, which makes sense: 16 bytes vs 8). But even at 1 million rows, we’re only talking tens of MB — trivial on modern systems.

Takeaway for Developers

The actual performance bottlenecks will almost always be query design, joins, and I/O — not whether your primary key is 4 bytes or 16.

So when someone says “UUIDs are slow”, you can point at query plans and actual numbers: they’re not.

TL;DR

See Also