English

Developer tools · UUID generator

Random UUID Primary Keys and B-Tree Fragmentation: What Really Happens

· Why it matters

uuid cryptography browser-apis

A B-tree diagram showing sequential inserts filling one page versus random inserts scattered across multiple pages
Original ToolAcre vector illustration

Random v4 keys insert into random index pages, and clustered indexes pay for it. This post explains the mechanism, the storage cost of text versus binary, and where time-ordered UUIDs change the picture.

Inserts slow down as the table grows — the symptom that sends DBAs looking at their key choice

Inserting a row with a random UUID as the primary key in a database with a clustered index causes the database to insert the new row at a random location within the B-tree structure. Sequential keys append to the rightmost leaf page, keeping all inserts within a small, cache-resident area. Random keys scatter inserts across the entire index, forcing the database to traverse and modify pages far apart in physical storage. As the table grows and the tree deepens, every insert touches more pages and causes more I/O operations. The operation that appeared cheap at a thousand rows becomes expensive at a million. This is not a theoretical problem; it manifests as measurable degradation in insertion throughput.

How a clustered B-tree fills — sequential keys append to the last page; random keys touch pages across the whole index

The mechanism is fundamental to how B-trees work because they maintain sorted key order within leaf pages. When you insert a row with key 10,001 into a table that has already stored keys 1 through 10,000, the database knows where that row belongs: at the end, in the existing rightmost page if there is space, or in a new page appended to the right. When you insert a row with a random UUID like 7524fae2-7dec-11d0-a765-00a0c91e6bf6 into the same table, the database must navigate the tree to find the leaf page containing keys in that UUID range, locate the exact position within that page and insert the row. If that page is full, it splits, moving half its contents to a new page and updating the parent node.

Page splits and cache pressure — why random insertion costs more I/O and why the effect grows with table size

Random keys create a worst-case insertion pattern because every insert lands at a random position in the tree instead of appending to the rightmost page. The database must search for the correct page, which typically costs several page reads—one per level of the tree. Then it must modify that page, which might trigger a split that propagates up the tree. More pages are modified, more writes occur, and the memory buffer pool fills with pages from different regions of the tree rather than staying focused on the active insertion point. Cache misses increase and I/O becomes the bottleneck, with the insertion rate plateauing as the table grows to larger scales.

Text or binary — 36-character strings versus 16-byte native uuid or binary(16) columns and the effect on index size

The cost is not uniform across database engines because different systems optimize page management differently. Databases with aggressive compression, small page sizes or in-memory operation may show smaller performance differences between sequential and random keys. Databases with large pages, mechanical disks or strict memory limits will show dramatic degradation. The problem is observable and measurable: measure insertion rates at 1,000 rows, 100,000 rows and 1,000,000 rows. If the rate per second drops sharply at the larger scales, you are experiencing the random-insertion penalty with your specific hardware and database configuration.

Time-ordered alternatives — how UUIDv7 and ULID keep uniqueness while inserting at the end of the index

UUIDs as strings consume 36 characters in text form or 16 bytes as a binary UUID type depending on storage format chosen. A 36-character string in a UTF-8 or ASCII column is 36 bytes, compared to 8 bytes for a 64-bit integer. The index on a string-UUID column is three times larger than an index on an integer column, assuming no compression techniques are applied. Larger indexes mean fewer index pages fit in the buffer pool, which means fewer cache hits when traversing the tree. A smaller index that fits in RAM performs better than a large index that must be read from disk on every query, regardless of insertion order or workload patterns.

Worked example — the same insert workload described for a random-key and a time-ordered-key table, qualitatively, without invented benchmarks

The storage difference matters significantly for both primary and secondary indexes because every secondary index that includes the primary key must store the full 36-character UUID or 16-byte binary UUID value. This makes secondary indexes substantially larger than indexes using an integer key for primary key lookups. Replication, backups and query result sets all grow proportionally with the larger index size. The ToolAcre UUID generator produces binary-compatible values; storing them as binary(16) or GUID types depending on the database saves space compared to varchar(36) and improves cache efficiency across the board. This storage optimization is critical for large-scale systems.

What this does not cover — heap-organised tables and databases where the primary key is not clustered, where the effect is smaller

A common optimization is to store the UUID as binary internally and display it as a string only when needed for APIs or user interfaces. The index and join operations work on the compact binary form; the API response or application code converts to string representation. Some databases offer built-in GUID or UUID types that handle this conversion automatically. Others require explicit casting operations. The performance difference between 36-byte and 16-byte columns is real: a table with a million rows and a 36-byte versus 16-byte UUID column key differs by 20 MB per index level, which can be the difference between an index fitting in L3 cache and requiring a memory fetch.

Takeaway: know your index before you pick a version — the ToolAcre generator produces random UUIDs; use the post to decide whether that fits your storage engine

The tradeoff between insertion performance, index size and query characteristics requires architectural decisions based on workload patterns. A sequential string key could be fast to insert if the strings are ascending for example, timestamp-based strings, but would consume the same space as random UUIDs and leak temporal information. A random UUID is semantically cleaner and has no timestamp component to leak, but is slower to insert into a clustered index and larger in storage overall. Time-ordered alternatives like UUIDv7 combine benefits by maintaining insertion locality while avoiding timestamp leaks in version 4 random identifiers.