todos os artigos
/// Coding ///

Should You Use UUIDs as Primary Keys? Performance Myths, Real-World Trade-Offs, and the Ideal Architecture

A deep dive into UUID vs. numeric primary key performance — B-Tree fragmentation, the numeric PK + UUID public ID pattern used by Stripe and GitHub, UUIDv7, ULID, and practical recommendations.

11 de December de 2025 · ~7 min de leitura · #uuid #primary-key #database ·

Database schema design is always full of trade-offs, but few debates are as persistent as the one around UUIDs vs. numeric primary keys. If you've ever wondered whether a UUID primary key actually slows down your database — and what to do about it — this post is for you.

Let’s go methodically through the myths, the technical reality, and the architecture used by big-scale platforms.

· · ·
01

Why do UUID primary keys get a bad reputation?

On paper, UUIDs look great: they’re globally unique, safe to expose, and avoid ID collisions across systems. But under the hood, they affect performance in a few very specific ways.

· · ·
02

Sequential vs. random inserts

One of the core reasons UUID primary keys are slower is how they behave inside a B-Tree index.

With a numeric auto-incremented primary key, inserts follow a predictable, sequential pattern. New rows always land at the end of the index — keeping it compact and efficient, with minimal page splits.

With UUIDv4, inserts are essentially random. New rows land unpredictably across many index pages, causing fragmentation and page splits on every write. The database must constantly reorganize existing pages to accommodate the new values.

The biggest penalty
B-Tree fragmentation from random UUID inserts is the single most significant performance cost of using UUIDv4 as a primary key. Write-heavy workloads feel this the most.
· · ·
03

The best of both worlds: numeric PK + UUID public ID

A widely-used pattern — employed by Stripe, Shopify, GitHub, and many others — is to combine:

  • An internal numeric primary key (auto-increment or sequence)
  • A public UUID field for external exposure (URLs, API endpoints, webhooks)

This provides the write speed of sequential keys while retaining the safety and opacity of UUIDs. Internal joins and foreign keys stay fast and compact; external consumers never see the internal integer.

Why this works so well

Concern Numeric PK + UUID public ID UUID-only PK
Insert performance Sequential, fast Random, fragmented
Foreign key size 4–8 bytes 16 bytes
External ID safety UUID hides internal structure UUID hides internal structure
Index memory pressure Small, lean Larger, more I/O
UUID lookup speed Fast via dedicated index Fast (it is the PK)

This pattern avoids all the classic pitfalls of using UUIDs as primary keys.

· · ·
04

Index and storage impact

The size difference is concrete:

  • UUID: 16 bytes per value
  • Integer (INT): 4 bytes
  • Big integer (BIGINT): 8 bytes

That difference compounds when the same field is used as a primary key, a foreign key in many child tables, and the clustered index on disk. Larger keys mean larger indexes, which means more memory pressure and more I/O per query.

In a table with millions of rows and dozens of related tables, the cumulative storage and memory difference between a UUID PK and a BIGINT PK can be significant — especially when the database’s buffer pool is the bottleneck.

· · ·
05

Want UUIDs only? Use UUIDv7 or ULID

If you genuinely need UUIDs as primary keys — for instance in a distributed system where nodes generate IDs independently — there are safer and faster alternatives to UUIDv4:

UUIDv7
Time-ordered UUID. Encodes a Unix timestamp in the most significant bits, making values monotonically increasing. Insert patterns are nearly as sequential as auto-increment integers, eliminating most B-Tree fragmentation.
RFC 9562 · 2024 standard
ULID
Universally Unique Lexicographically Sortable Identifier. 128-bit, URL-safe, millisecond precision. Sorts correctly as a string, making it index-friendly and human-readable in logs.
ulid.github.io

Both maintain index locality — new values always land near the end of the B-Tree, keeping inserts efficient even at scale.

UUIDv7 in .NET
Starting with .NET 9, Guid.CreateVersion7() is built into the BCL — no third-party package needed. For earlier versions, libraries like UUIDNext or Medo.Uuid7 provide the same functionality.
· · ·
06

Query flow with UUID public IDs

Even though the internal primary key is numeric, your APIs can fully expose UUIDs without architectural penalty. The typical request flow:

  1. Client sends a request containing a UUID (e.g. GET /orders/01932f3a-...)
  2. API queries the database using a dedicated index on the UUID column
  3. Database resolves the numeric PK from that index in a single lookup
  4. All internal JOINs and foreign key relationships use the efficient numeric key
  5. Response returns to the client using the UUID — the internal integer is never exposed

The UUID index lookup adds one extra index read compared to querying by numeric PK directly, but in practice this is negligible. The wins in write performance, index size, and external security far outweigh this tiny overhead.

· · ·
07

Practical recommendations

Best practice — most applications
Use a numeric primary key (INT or BIGINT auto-increment) paired with a UUID public_id column. Index the UUID column. Expose only the UUID externally. You get fast writes, small indexes, secure URLs, and lightweight foreign keys.
✓ Recommended default
UUID-only — when it makes sense
Use UUIDv7 or ULID when designing a distributed system that requires global uniqueness across nodes, you won't rely heavily on relational foreign-key joins, and you accept the larger storage footprint.
⚠ Specific use cases
Avoid — UUIDv4 as PK
UUIDv4 as a primary key is almost never the right choice. Random inserts cause B-Tree fragmentation, page splits, larger indexes, and higher memory pressure — all without any benefit over UUIDv7 or the numeric+UUID pattern.
✗ Avoid in write-heavy tables

Pick the right tool for the right problem

The debate around UUID primary keys often stems from misunderstandings about what UUIDs actually cost and where that cost comes from.

UUIDv4 as a primary key? Works, but harms write performance through B-Tree fragmentation. Avoid it in write-heavy tables.

Numeric PK + UUID public ID? The most robust and scalable design for everyday relational applications. Used in production by Stripe, Shopify, GitHub and many others.

UUIDv7 or ULID? A great alternative when you truly need distributed, globally unique, sortable identifiers — with a much smaller performance penalty than UUIDv4.

With the full engineering picture in mind, you can choose the right structure for your system with confidence.

 Categorias

 Tópicos deste artigo

URL copiada!