Skip to content
Technical Cases

UUID v4 vs v7: when to migrate and what real gain to expect

Why random v4 UUIDs fragment your database index and how time-ordered v7 returned +260% throughput in the sales-reporting module of a European retail SaaS.

Technical CasesJohnny Carreiro·January 29, 2026·4 min read

A client came to us convinced they needed to rewrite the whole system in Go. The symptom lived in the sales-reporting module of a European retail SaaS: reports got slower as the range grew (month → quarter → semester), and that latency was bleeding into inserts and other internal services. Profiling and benchmarking pointed the bottleneck elsewhere — at the database, not the language — and the fix did not change a single line of language.

The UUID v4 problem

UUID v4 is random by design. Each new identifier lands in an unpredictable position in the key space. That seems harmless until you remember how a PostgreSQL B-tree index works: it keeps keys ordered across pages. When you insert keys in order (or nearly so), inserts land on the last pages and the index grows compactly. When you insert random keys, each insert can land on any page — scattering writes across the whole tree.

The result is fragmentation: the index bloats, pages stay partially filled, the database cache becomes less efficient because "hot" pages are scattered, and each insert can force a page split. At low volume this does not show. In distributed databases with billions of records, it becomes the dominant bottleneck.

Why v7 fixes it

UUID v7 (standardized in RFC 9562) places a millisecond timestamp in the most significant bits, followed by random bits. In practice, this makes UUIDs time-orderable: identifiers generated in sequence stay close in the key space.

For the B-tree index, that changes everything. Inserts land on adjacent pages again, as a sequential key would, but you keep the UUID advantages — distributed generation with no central coordination, no exposed record count, no practical collision. It is the best of both worlds: the global uniqueness of a UUID with the locality behavior of a sequential key.

The real gain

In this case, the migration to v7 delivered +260% insertion throughput and around -40% on range query time — because records close in time are now close in the index and on disk. All of that with zero downtime and rollback prepared, instrumented with Jaeger to validate every step in distributed tracing. The full rewrite to Go the client was considering would have cost months and solved nothing: the bottleneck was never in the application language.

How to migrate safely

Migrating a primary key on large tables — distributed ones, no less — takes care. The path we used avoided a big-bang and a destructive schema migration:

  1. A lookup table mapping the v4 id ↔ the new v7 uuid — the source of truth for the correspondence during the transition. A custom Rust tool generated the v7s and populated the map for the billions of records, in controlled windows.
  2. Swap the id for the v7 progressively across the tables, reading from the lookup — no new column living alongside the old one.
  3. Propagate the v7 through relationships and references before touching the index: both traditional foreign keys and informal references (a value "referenced" across separate databases, with no formal FK) start pointing to the v7.
  4. Rebuild the index gradually, as the v7 had already propagated — avoiding the cost and the lock of recreating everything at once.
  5. Measure before and after — without a comparative benchmark, you do not know whether you gained anything.

It wasn't the most obvious path, but it worked with zero downtime. Since a v7 carries a timestamp, you can't derive it from a v4: a correspondence map really is the central piece — and every case calls for its own plan.

When it is worth it

Migrating UUID v4 to v7 is worth it when: you have large tables (tens of millions of rows or more), you use UUID as a primary key, and you observe slow inserts or index bloat relative to your data volume. For new projects, it is simple: start on v7 and avoid the problem from day one.

It is not worth the effort on small tables, where fragmentation is irrelevant. As always in performance, the decision is guided by the metric, not by fashion. But when the symptom matches, migrating to v7 is one of the best returns on effort there is — it fixes the root cause without rewriting the application.