Skip to main content

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples in this guide work on VillageSQL. Install Now →
Primary key decisions are hard to undo. The wrong choice early — a schema migration away from INT AUTO_INCREMENT, say, once you’ve got foreign keys scattered across twelve tables — is painful. This guide covers the three main approaches, when each is the right tool, and what VillageSQL adds to the mix.

The MySQL Default: AUTO_INCREMENT

AUTO_INCREMENT integers are the default for a reason. They’re small (4 or 8 bytes), insert fast, sort naturally, and are immediately human-readable in a debugger or log file.
InnoDB’s clustered index stores rows in primary key order. With an auto-increment key, every new row goes at the end of the B-tree. That means no page splits, no index fragmentation, and fast sequential inserts. For most single-database applications, this is the right choice. The problems with AUTO_INCREMENT emerge when you scale or integrate:
  • Row count exposure: SELECT COUNT(*) from an ID sequence tells an API client how many orders exist. Sequential IDs in URLs let users enumerate records.
  • Distributed generation: Generating unique IDs across multiple writers requires coordination — a sequence server, a designated writer, or manual ID ranges.
  • Data merges: Merging two tables from different databases produces ID collisions. You’re doing a renumber.
If none of those apply, AUTO_INCREMENT is probably the right answer. Keep it.

When You Need Globally Unique IDs

The standard solution is UUIDs: 128-bit random identifiers that can be generated independently on any node without coordination and won’t collide in practice. MySQL’s built-in UUID() returns a v1 UUID as a CHAR(36) string. You can store it directly, but there’s a cost: CHAR(36) is 36 bytes versus 16 bytes for the binary equivalent, and random UUIDs (v4) cause InnoDB index fragmentation because new rows don’t land at the end of the index — they land anywhere, forcing page splits. The standard workaround is BINARY(16) with manual conversion:
The 1 flag in UUID_TO_BIN() reorders timestamp bits so v1 UUIDs sort chronologically. That reduces fragmentation for v1 UUIDs specifically, but the manual encode/decode adds friction to every query.

With VillageSQL: Native UUID Type and Sortable Versions

VillageSQL’s vsql_uuid extension adds a native UUID column type and generation functions for UUID v1 through v7. No manual encode/decode, and UUID v7 solves the insert fragmentation problem cleanly.
The UUID type stores 16 bytes on disk and displays as the standard 36-character string format automatically. No UUID_TO_BIN or BIN_TO_UUID in your queries. For deterministic IDs — the same input always produces the same UUID — use v3 or v5:
For details on UUID storage mechanics and the insert fragmentation problem, see UUIDs in MySQL.

UUID version reference

Additional introspection functions: UUID_VERSION(uuid) returns the version number of any UUID, useful when you receive IDs from external systems and want to validate or route them. UUID_COMPARE(uuid1, uuid2) returns -1, 0, or 1 for explicit lexicographic comparison.

Choosing a Strategy

For most teams starting a new project: BIGINT AUTO_INCREMENT for internal tables, native UUID with UUID_V7() for tables with public-facing IDs or data that might be merged across systems.

Frequently Asked Questions

When should I use UUID_V7 vs UUID_V4?

Use UUID_V7() for primary keys on high-write tables. It embeds a timestamp in the high bits, so new UUIDs always sort after existing ones — this keeps InnoDB from splitting index pages on insert. Use UUID_V4() only when you need random IDs with no ordering semantics (e.g., one-time tokens).

Can I use UUID as a DEFAULT expression?

Not currently. MySQL doesn’t allow non-deterministic functions in DEFAULT expressions. Generate the UUID in each INSERT statement rather than in the column definition.

Should I use UUID for foreign keys too?

Yes, but be aware of the storage cost: a UUID foreign key is 16 bytes per row in the referencing table, compared to 4–8 bytes for an integer. On tables with millions of rows and multiple UUID foreign keys, that adds up. For pure internal references, an AUTO_INCREMENT + integer foreign key is leaner.

What’s the difference between UUID_V3 and UUID_V5?

Both generate deterministic UUIDs from a namespace and a name — the same inputs always produce the same output. v3 uses MD5; v5 uses SHA-1. Prefer v5 for new code since SHA-1 is marginally stronger. Use v3 only when you need to match IDs generated by a system that uses v3.

Troubleshooting

See also