For years, developers using PostgreSQL faced a frustrating tradeoff when choosing primary key types. Auto-incrementing integers gave you great performance but exposed sequential IDs in URLs and made distributed systems harder to build. Random UUIDs (v4) provided global uniqueness and URL obscurity but caused terrible index fragmentation that degraded database performance as tables grew.
PostgreSQL 18 changes this with native support for UUID v7, a new UUID variant that combines the best of both approaches. UUID v7 generates globally unique identifiers that are naturally ordered by time, eliminating the B-tree index problems that plagued random UUIDs while maintaining all their distributed-system benefits.
This guide shows you how UUID v7 works, why it performs dramatically better than UUID v4, and how to use it effectively in PostgreSQL 18.
Prerequisites
Before proceeding with this article, ensure you have PostgreSQL 18 installed on your system:
This guide assumes basic familiarity with PostgreSQL, primary keys, database indexes, and UUID data types. You should be comfortable executing SQL commands and interpreting query results.
Understanding the UUID v4 performance problem
UUID v4 generates completely random 128-bit values. When used as a primary key, each insert targets a random location in PostgreSQL's B-tree index instead of appending to the end like sequential integers do. This creates several performance problems:
Excessive page splits: Sequential IDs cause 10-20 page splits per million records. UUID v4 causes 5,000-10,000+ splits—that's 500 times more. Each split triggers additional I/O operations.
Write amplification: A single insert can trigger 5-10 times more physical I/O due to cascading page splits and B-tree rebalancing.
Index bloat: Random inserts fragment the index, causing pages to average 69% full instead of approaching 100%, wasting disk space.
Poor cache utilization: Random inserts spread data across many pages instead of concentrating on a few hot pages, making your buffer cache less effective.
Buildkite observed a 50% reduction in Write Ahead Log generation after switching from random UUIDs to time-ordered ones. The performance impact becomes particularly noticeable once your tables grow larger than available RAM.
How UUID v7 solves the problem
UUID v7 was standardized in RFC 9562 (published May 2024) to address these performance issues. The key innovation: encoding a Unix timestamp in the most significant bits.
Here's the bit structure:
The components:
- unixtsms (48 bits): Unix timestamp in milliseconds (approximately 8,900 years before overflow)
- ver (4 bits): Version field set to
0111(7 in decimal) - rand_a (12 bits): Random data or sub-millisecond timestamp fraction
- var (2 bits): Variant field set to
10for RFC 9562 compliance - rand_b (62 bits): Additional random data for uniqueness
Because the timestamp occupies the most significant bits, UUID v7 values sort naturally by creation time. This transforms the B-tree insertion pattern from completely random to primarily sequential, eliminating most performance problems.
PostgreSQL 18's UUID v7 implementation
PostgreSQL 18 implements UUID v7 with an important enhancement: the 12-bit rand_a field stores sub-millisecond precision rather than purely random data. This serves two purposes:
- Provides a monotonically increasing counter within each millisecond
- Ensures monotonicity even when the system clock moves backward
If you generate multiple UUID v7 values within the same millisecond from the same PostgreSQL backend, they're guaranteed to be in strictly increasing order. This guarantee only applies within a single backend—UUIDs from different connections aren't guaranteed to be monotonic relative to each other, though they'll typically be ordered correctly due to the timestamp.
Generating UUID v7 values
Let's start using UUID v7 in PostgreSQL 18. The database provides a simple uuidv7() function that generates new UUID v7 values.
Start by creating a test database to experiment with UUID v7:
Connect to the database:
Then create a test table to experiment with:
Insert some test data:
Now query the results to see the generated UUID v7 values:
Notice how the UUID values start with similar prefixes (01941c04-418...). This is the timestamp portion, and it increases slightly with each insertion. The similarity indicates these records were created within milliseconds of each other.
You can also generate UUID v7 values directly without a table:
The monotonic increase in values is subtle but present. PostgreSQL guarantees these five UUIDs are in strictly increasing order because they were generated sequentially within the same backend session.
Generating UUID v7 with specific timestamps
One powerful feature of PostgreSQL 18's uuidv7() function is the ability to generate UUIDs for specific timestamps. This is useful for backfilling historical data, testing time-based behavior, or generating UUIDs that represent past or future events.
The function accepts an optional interval parameter that shifts the timestamp:
You can see how the timestamp portion changes based on the interval. The UUID for yesterday has a smaller timestamp prefix (01940f30) than the current time (01941c04), and next year has a larger one (0195e4d4).
This feature is particularly useful when migrating data from legacy systems:
You don't need to run this example since the legacy_products table doesn't exist. This illustrates migrating historical data while preserving chronological order.
This generates UUID v7 values that accurately reflect when each product was originally created, maintaining chronological ordering in the new table.
Extracting timestamps from UUID v7
PostgreSQL 18 enhances the uuid_extract_timestamp() function to work with UUID v7 values. This lets you extract the embedded timestamp, which is useful for debugging, auditing, or working with UUIDs from external systems.
The extracted timestamp matches closely with the created_at column, though the UUID timestamp has only millisecond precision while the actual TIMESTAMPTZ column preserves microseconds.
You can also extract the UUID version to verify you're working with UUID v7:
Comparing UUID v4 and UUID v7
PostgreSQL 18 also adds uuidv4() as an alias for the existing gen_random_uuid() function. This gives you consistent naming across UUID versions and makes comparisons clearer.
Let's create two tables with identical schemas but different UUID types to see the practical differences:
Generate sample data for both tables:
Now compare the UUID patterns:
The UUID v4 values show no pattern—they're completely random. The UUID v7 values all start with similar prefixes because they share the same timestamp portion.
Check the index sizes:
With just 10,000 rows, UUID v7 already shows a smaller index size due to better page fill rates. This difference becomes more dramatic as tables grow larger.
Test query performance for range scans:
The UUID v7 query executes faster (0.129ms vs 0.230ms) and requires significantly fewer buffer hits (3 vs 101). The time-ordering of UUID v7 values means they naturally align with the chronological order_date filter, allowing PostgreSQL to scan fewer index pages to find the matching rows.
Understanding UUID v7 limitations
While UUID v7 offers significant advantages, you should understand its limitations and tradeoffs before adopting it.
Timestamp exposure: UUID v7 embeds the creation timestamp directly in the identifier. If you're using UUIDs specifically to make record creation times non-obvious, UUID v7 defeats that purpose. Anyone can extract the timestamp using uuid_extract_timestamp() or by decoding the first 48 bits.
Uniqueness depends on randomness: UUID v7 still relies on random bits for uniqueness. While 74 bits of randomness (compared to UUID v4's 122 bits) is sufficient for most applications, extremely high-frequency generation across many systems could theoretically cause collisions. In practice, collision probability remains astronomically low.
Monotonicity guarantees are limited: PostgreSQL 18 guarantees monotonicity only within a single backend process. If you generate UUID v7 values from multiple database connections simultaneously, they are not guaranteed to be strictly increasing relative to each other, though they will typically be ordered correctly due to different millisecond timestamps.
Clock dependency: UUID v7 quality depends on your system clock. If your server's clock jumps backward (due to NTP corrections or manual changes), newly generated UUID v7 values might sort before previously generated ones. PostgreSQL's implementation mitigates this somewhat with sub-millisecond counters, but dramatic clock changes can still cause ordering anomalies.
Not a direct drop-in for auto-increment integers: While UUID v7 performs much better than UUID v4, it's still not as fast as auto-incrementing integers for pure insertion performance. The 16-byte size also means indexes and foreign keys take more space than 8-byte BIGINT columns.
Final thoughts
UUID v7 in PostgreSQL 18 represents a significant improvement for applications that need globally unique identifiers. By encoding timestamps in the most significant bits, UUID v7 transforms random UUID insertion from a performance liability into a reasonable choice for primary keys.
The benefits are clear: faster insertions, smaller indexes, better query performance, and natural chronological ordering—all while maintaining the distribution advantages and URL-obfuscation properties that make UUIDs attractive in the first place.
PostgreSQL 18's implementation adds extra value with guaranteed monotonicity within backend sessions and the convenient uuid_extract_timestamp() function for debugging and auditing. With PostgreSQL 18 now available, UUID v7 should become the default choice for new applications that need UUID-based identifiers.
For existing applications using UUID v4, consider migrating to UUID v7 for new tables while you evaluate the effort required to migrate existing data. The performance improvements are substantial enough that migration often pays for itself in reduced infrastructure costs for write-heavy workloads.