Default to a surrogate key — an auto-incrementing integer, bigint, or UUID — for every entity's primary key, and enforce your "natural" identifier (email, SKU, VIN) as a separate unique constraint instead. Natural keys look stable until the business rule behind them changes, and by then every foreign key pointing at them has to change too.

Use a surrogate key (auto-increment or UUID) as the primary key on every table. Keep the natural key as a unique, indexed column — not the key everything else points to.

What You're Actually Choosing Between

A natural key is an attribute that already carries meaning in the business: an email, a SKU, a VIN, a national ID number. A surrogate key is a system-generated identifier with no meaning outside the database: an auto-increment integer, a bigint sequence, a UUID. The choice determines what every other table joins against.

This decision matters more than most schema calls because primary keys propagate. Once a foreign key references orders.customer_email, that email is now baked into every order, invoice, and shipment row. Changing it later means touching every table that ever pointed at it — not just the customers table.

That's the trap this article is named for. A column type you chose on day one (see our guide to choosing column types for typed attributes) is a local decision. A primary key is a structural one, because it's the thing every relationship in your entity-relationship model is built on top of. If you haven't settled what actually counts as a distinct entity in your domain, that's worth resolving first — see what counts as an entity — because the natural-vs-surrogate question only bites once you know what you're keying.

Here's the asymmetry that makes this decision one-directional in practice:

  • Starting with a surrogate key and later adding a natural-key unique constraint is a five-minute migration.
  • Starting with a natural key and later swapping it for a surrogate key means rewriting every foreign key, every join, every API contract, and every cached reference across the system.

You can always add structure. Removing structure that other tables depend on is where migrations go to die.

Why Natural Keys Keep Betraying You Later

Natural keys fail because the "stable and unique" properties you assumed at design time were assumptions about how the business currently operates, not database guarantees. Businesses re-org, merge, rebrand, and let people change their names and jobs — all faster than anyone updates the schema that assumed otherwise.

Email is the clearest offender. It looks perfect: unique, human-readable, already validated at signup. Then a customer changes jobs and their @oldcompany.com address bounces. Or two people share a household inbox. Or — the case that breaks the most schemas — the business itself changes: a company reorganization means an email address gets reassigned to a different employee, but the historical orders and support tickets keyed to that email now belong to the wrong human.

"Natural" key candidateAssumed to beActually isCommon failure mode
Email addressPermanent, unique per personReassignable, changeable, shareablePerson changes job/name; address gets recycled or bounces
SKU / product codeFixed per productRenumbered on re-platform or M&AMerged catalogs collide; vendor changes numbering scheme
SSN / national IDGlobally uniqueJurisdiction-specific, sometimes reissuedInternational customers have none; numbers get reissued
VIN (vehicle ID)Unique per vehicleOccasionally duplicated across manufacturers/erasPre-1980s or grey-market vehicles collide
Phone numberTied to one personRecycled by carriers within monthsNew owner inherits the old owner's account history
UsernameChosen once, permanentUsers request changes constantlyRename breaks every deep link and audit log entry

The deeper issue is a category error: you're using an attribute that answers a business question ("how do we reach this person?") as if it answered an identity question ("which row is this, forever?"). Those are different jobs, and conflating them is exactly the trap covered in the complete guide to Jobs to Be Done — an email's job is communication, not identity, and forcing it to do both means it fails at identity the moment the communication need changes.

The same logic applies over time, not just across entities. A customer's journey from trial to renewal to expansion often spans a job change, a name change, or a company acquiring another — any of which can invalidate the "stable" attribute you keyed on at signup. Identity has to survive the whole journey, not just the moment of first contact.

The Surrogate Key's Hidden Costs

Surrogate keys aren't free — they trade one set of problems for another. You still need a unique constraint on the natural key (or you'll get duplicate customers), you add a join hop between "the row" and "the business meaning," and if you expose the wrong kind of surrogate key in a URL, you leak information you didn't mean to.

You still have to model the natural key. Making id the primary key doesn't remove the requirement that email be unique — it just moves that rule from PRIMARY KEY to a UNIQUE constraint. Skip this step and a surrogate key gives you the worst outcome: duplicate customer rows with different auto-increment IDs and no database-enforced way to catch it.

Every query needs an extra join. Looking up a customer by email now means SELECT * FROM customers WHERE email = ? instead of a direct primary-key hit — usually fine with an index, but it's a real second lookup path you have to maintain and keep indexed.

Sequential surrogate IDs leak business information. If your invoice URLs are /invoices/1048, a competitor — or anyone — can watch the counter and estimate your order volume, sign-up rate, or growth curve just by creating two accounts a week apart and subtracting the IDs. This is a well-documented enumeration problem, not a hypothetical one, and it's a big part of why UUIDs became popular for anything customer-facing.

Merges still hurt, just differently. If two companies each ran their own auto-increment customers table and you need to combine them, both sets of IDs collide (both have a row with id = 1). A natural key doesn't save you here either — you'd have deduplication work either way. The difference is that with surrogate keys the collision is obvious and mechanical (renumber and remap foreign keys); with natural keys it's semantic and ambiguous (is this the same "Acme Corp" or a different one?).

UUID vs Auto-Increment: The Distributed Systems Tradeoff

Choose an auto-increment integer or bigint when you have one writable database and want compact, sequential, index-friendly keys. Choose a UUID — or a time-sortable hybrid like ULID or a Snowflake-style ID — when multiple services or database shards need to generate IDs independently, without asking a central sequence for the next number.

This is the tradeoff that actually splits engineering teams, and it's worth understanding as a PM because it constrains your architecture, not just your schema.

Auto-increment's advantage is locality. A bigint sequence is 8 bytes, always increasing, and new rows always insert at the end of the index — which is exactly how B-tree indexes like to be written to. Vlad Mihalcea's widely cited database performance benchmarks show that random-order inserts (which is what UUID v4 produces) cause far more index page splits and disk I/O than sequential inserts, because the database keeps having to reshuffle pages instead of appending to the end.

Auto-increment's weakness is coordination. A single sequence means a single source of truth for "what's the next number," which is awkward the moment you shard your database or run multiple writable regions. This is precisely the problem Twitter solved by open-sourcing Snowflake in 2010 — a 64-bit, time-ordered ID scheme that lets many machines generate unique, roughly sortable IDs without talking to each other. Instagram published a near-identical approach for the same reason: sharded Postgres meant no single sequence could hand out the next integer fast enough, or at all, across shards.

UUID v4's advantage is that any node can generate one, with no coordination. That's why it's the default in microservice architectures, offline-first mobile apps, and multi-region databases. Distributed-SQL vendors like CockroachDB explicitly warn against pure sequential primary keys in a sharded cluster, because every insert lands on whichever node owns the "latest" range — a hotspot — while random UUIDs spread writes evenly.

UUID v4's weakness is randomness itself. It's 16 bytes (double a bigint), it's not sortable by creation time, and — per Mihalcea's benchmarks — its randomness is the same property that causes index fragmentation. This is why ULID and KSUID exist: they encode a timestamp in the high bits and randomness in the low bits, giving you UUID-style distributed generation with auto-increment-style sortability and index locality.

PropertyAuto-increment (int/bigint)UUID v4ULID / KSUIDSnowflake-style
Storage size4–8 bytes16 bytes16 bytes8 bytes
Sortable by creation timeYesNoYesYes
Safe to expose in a public URLNo — reveals volume/sequenceYesYesMostly (encodes rough timestamp)
Generated without central coordinationNoYesYesYes (needs a machine/worker ID)
Index write locality (insert performance)ExcellentPoorGoodGood
Human-readable in logs/debuggingEasyHardModerateModerate

Whichever you choose, the underlying column type matters for storage and index behavior — a UUID stored as text instead of a native UUID type roughly doubles its footprint and loses index efficiency, which is the kind of decision our guide to typed attributes and column types covers in more depth.

The Default, and the Named Exceptions

Default to a surrogate primary key on every entity, with a separate unique constraint on whatever natural identifier the business actually cares about. Reserve natural keys as the primary key only for a small set of cases where the "key" is externally governed, structurally composite, or contractually guaranteed — not just "looks stable today."

The exceptions worth naming:

  1. Reference and lookup tables governed by an external standard. ISO 3166 country codes, ISO 4217 currency codes, or your own fixed enum-like tables (status codes, plan tiers) are genuinely stable because a standards body — not your business — owns the meaning. Using US, GB, USD as the primary key is fine and saves a join.
  2. Pure junction (many-to-many) tables. A table that only exists to link student_id and course_id can use the composite pair as its primary key — there's no independent "enrollment identity" worth surrogate-keying unless you need to reference the enrollment row itself elsewhere.
  3. Idempotency keys from a trusted external system. If a payment processor guarantees a transaction ID is unique and immutable by contract (not just by convention), treating it as a natural key for a payments-ledger table is reasonable — the "external system" has effectively become the authority, the way ISO owns country codes.
  4. Append-only event logs where the event ID is the event. In event-sourced systems, the event's own identity (often a ULID assigned at creation) is both the natural and surrogate key — there's no separate "business meaning" to diverge from the identifier.

Everywhere else — customers, products, orders, employees, accounts — use a surrogate key. The rule of thumb: if you can imagine a plausible business reason the value would need to change (rename, reassign, merge, re-issue), it doesn't belong as your primary key.

Why This Is Hard to Undo — and How to Make It Safer

Primary key migrations are expensive because the key isn't a local column — it's referenced by every foreign key, every index, every cached ID, and often every URL and API contract built on top of it. Undoing the decision means touching every one of those surfaces at once, usually while the system stays live.

Concretely, migrating off a natural primary key means:

  • Adding a new surrogate column to the parent table and backfilling it.
  • Adding a matching column to every child table that references the old key, and backfilling those too.
  • Rewriting every foreign key constraint to point at the new column.
  • Updating every application query, ORM model, and cached reference.
  • Coordinating a cutover window where writes don't land on the old key mid-migration.

None of that is impossible — teams do it — but it's the kind of multi-week, multi-team project that a five-minute decision at schema design time could have avoided entirely. That asymmetry is the whole argument for defaulting to surrogate keys: the downside of "wrong" is a unique constraint you forgot to add; the downside of the reverse mistake is the migration above.

This is also why the decision belongs in the data model, not in application code. If your entities, relationships, and primary keys are only visible inside individual services' ORM files, nobody sees the blast radius of a key choice until the migration is already painful. A single source of truth for entities and their relationships — the kind an ERD-first modeling process is meant to produce — is what lets a PM or architect actually see, before launch, which tables would be affected if a given key had to change.

Where Prodinja fits

This is exactly the moment in a data-modeling exercise where the tradeoff should be explicit rather than inherited from whatever your ORM scaffolds by default. Prodinja's Data Modelling tool lets you designate the primary key for each entity as you define it, and then shows that key anchoring every relationship in the generated DDL — so you can see, before a single migration runs, which foreign keys would need to change if you picked a natural key that later turned out not to be stable. It's a way to have this conversation on a whiteboard instead of in a production incident.

If you're building out the full picture of your domain — not just this one decision — our complete guide to data modeling walks through entities, relationships, and typing as one connected process rather than a series of isolated column choices.

Key Takeaways

  • Default to a surrogate key (auto-increment, bigint, or UUID) as the primary key on every entity, and enforce natural identifiers as separate unique constraints.
  • Natural keys fail when the business changes, not when the data is wrong — emails get reassigned, SKUs get renumbered, phone numbers get recycled.
  • Surrogate keys aren't free: you still need a unique constraint on the natural key, you add a join hop, and sequential IDs exposed in URLs leak business volume.
  • Auto-increment wins on index locality; UUID wins on coordination-free distributed generation. ULID/KSUID split the difference by encoding a timestamp alongside randomness.
  • Name your exceptions deliberately: externally governed codes (ISO 3166, ISO 4217), pure junction tables, and contractually guaranteed external IDs are the legitimate cases for natural primary keys.
  • The cost of this decision is asymmetric — adding a unique constraint later is trivial; unwinding a natural primary key that other tables depend on is a multi-week migration.
  • Make the tradeoff visible at design time, in your entity-relationship model, rather than letting it default to whatever your framework scaffolds.

Frequently Asked Questions

Should I use email as a primary key?

No — use a surrogate key and add a unique constraint on email instead. Email addresses get reassigned, changed, and occasionally shared, and every table that references the primary key would need to change whenever the email does.

Is UUID or auto-increment better for a primary key?

It depends on whether you write from one database or many. Auto-increment is faster to index and smaller in a single-writer system; UUID (or ULID) lets multiple services or shards generate IDs independently without coordinating, at the cost of larger storage and less index locality.

Can I change a primary key later without breaking everything?

Technically yes, but it requires backfilling a new column on the parent table and every child table that references it, rewriting foreign keys, and coordinating a cutover — which is why getting it right at design time is far cheaper than migrating later.

What's the difference between a natural key and a surrogate key?

A natural key is an attribute that already has business meaning outside the database, like an email or SKU; a surrogate key is a system-generated identifier, like an auto-increment integer or UUID, that exists solely to identify the row.

Are UUIDs bad for database performance?

UUID v4's randomness can cause more index fragmentation and disk I/O on inserts compared to sequential keys, according to widely cited database benchmarking work. Time-ordered variants like ULID or KSUID largely avoid this by keeping IDs roughly sortable while still allowing independent generation.