Choosing a primary key looks simple until a PostgreSQL table starts handling a large number of inserts.
For a small application, these choices may appear interchangeable:
BIGINT GENERATED BY IDENTITY
UUIDv4
UUIDv7
They are not identical from an indexing and storage perspective.
PostgreSQL 18 adds native uuidv7() generation. UUIDv7 is a timestamp-ordered UUID format, while UUIDv4 remains random. PostgreSQL's documentation describes UUIDv7 as time-ordered and notes that PostgreSQL can generate both UUIDv4 and UUIDv7 values natively.
That makes PostgreSQL 18 a useful point to revisit an old question:
How does UUIDv7 behave compared with a sequential numeric key when both are used as primary keys?
The answer should not be based on assumptions such as "UUIDs are slow" or "UUIDv7 is always faster."
The right approach is to measure:
This article focuses on how to build that benchmark and how to interpret the results without turning one workload into a universal performance claim.
Why UUIDv7 Exists
UUIDs are useful in distributed systems because they can be generated independently without relying on a single database sequence.
PostgreSQL describes UUIDs as 128-bit identifiers and notes their usefulness in distributed systems because uniqueness does not depend on a single database sequence.
The problem with UUIDv4 is that its values are random.
Consider:
UUIDv4
7f...
21...
c9...
03...
a8...
When these values are inserted into a B-tree index, new keys can arrive at many different positions.
A sequential numeric key behaves differently:
1001
1002
1003
1004
1005
New values naturally progress toward the end of the index.
UUIDv7 attempts to combine distributed UUID generation with temporal ordering.
Conceptually:
UUIDv4
Random identifier
|
v
Random index placement
UUIDv7
Timestamp + randomness
|
v
Approximately time-ordered identifier
|
v
More sequential index behavior
PostgreSQL 18's uuidv7() function generates timestamp-ordered UUIDs using millisecond precision plus sub-millisecond and random components.
Sequential Keys Are Not the Same as UUIDv7
It is important not to describe UUIDv7 as a direct replacement for a database sequence in every scenario.
A sequential numeric key might be:
CREATE TABLE orders
(
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);
UUIDv7 might be:
CREATE TABLE orders
(
id uuid PRIMARY KEY DEFAULT uuidv7()
);
Both are ordered differently from UUIDv4, but they have different properties.
| Property | BIGINT Identity | UUIDv4 | UUIDv7 |
|---|
| Size | 8 bytes | 16 bytes | 16 bytes |
| Globally generated | Database sequence | Yes | Yes |
| Random ordering | No | Yes | No, time-ordered |
| Distributed generation | Requires design | Yes | Yes |
| Native PostgreSQL generation | Yes | Yes | Yes |
| Time information | No | No | Embedded |
| Natural numeric ordering | Yes | No | Temporal ordering |
| Suitable for distributed systems | Depends | Strong | Strong |
The UUID column type itself is still 128 bits regardless of whether the stored value is version 4 or version 7.
What Should the Benchmark Measure?
A meaningful benchmark should not ask only:
Which key inserts faster?
Instead, measure several dimensions.
Insert Performance
How quickly can rows be inserted?
Index Size
How much storage does the primary-key index require?
Index Locality
How closely does key order correspond to physical insertion order?
Range Queries
How efficiently can the database retrieve a time-oriented range?
Point Lookups
Does the identifier type materially affect lookup behavior for the tested workload?
Concurrent Inserts
What happens when multiple clients insert rows simultaneously?
Storage Overhead
How does the 16-byte UUID compare with an 8-byte integer once indexes and foreign keys are included?
These measurements provide a much more useful picture.
Build a Controlled Test Table
Start with three tables.
CREATE TABLE orders_bigint
(
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
UUIDv4:
CREATE TABLE orders_uuidv4
(
id uuid PRIMARY KEY DEFAULT uuidv4(),
customer_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
UUIDv7:
CREATE TABLE orders_uuidv7
(
id uuid PRIMARY KEY DEFAULT uuidv7(),
customer_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
PostgreSQL 18 provides uuidv4() as an alias for gen_random_uuid() and adds uuidv7() for time-ordered UUID generation.
Keep the non-key columns identical.
Otherwise, you are not comparing the key strategies fairly.
Generate the Same Workload
Suppose the benchmark inserts:
10,000,000 rows
Do not run one table with a simple row and another with a more complicated object.
Use the same logical data:
customer_id
amount
created_at
Only change:
id
This gives you a controlled experiment.
For example:
INSERT INTO orders_uuidv7
(
customer_id,
amount
)
SELECT
(random() * 100000)::bigint,
round((random() * 10000)::numeric, 2)
FROM generate_series(1, 1000000);
Repeat the same workload against the other tables.
Measure Insert Time
A simple measurement can start with:
\timing on
INSERT INTO orders_uuidv7
(
customer_id,
amount
)
SELECT
(random() * 100000)::bigint,
round((random() * 10000)::numeric, 2)
FROM generate_series(1, 1000000);
However, for a serious benchmark, do not rely on a single execution.
Run multiple trials.
For example:
Warm-up
|
v
Trial 1
|
v
Trial 2
|
v
Trial 3
|
v
Trial 4
|
v
Trial 5
Then compare median and percentile results.
Measure Index Size
The primary key creates a B-tree index.
Measure it directly:
SELECT
pg_size_pretty(pg_relation_size(
'orders_bigint_pkey'
)) AS index_size;
For UUIDv4:
SELECT
pg_size_pretty(pg_relation_size(
'orders_uuidv4_pkey'
)) AS index_size;
And UUIDv7:
SELECT
pg_size_pretty(pg_relation_size(
'orders_uuidv7_pkey'
)) AS index_size;
The exact index names depend on how the tables were created.
You can discover them with:
SELECT
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes
WHERE relname IN
(
'orders_bigint',
'orders_uuidv4',
'orders_uuidv7'
);
Do not assume that a 16-byte key automatically translates into exactly twice the complete index size.
B-tree pages contain additional metadata and tuple information.
Measure the actual relation size.
Measure Table and Index Together
For a broader storage comparison:
SELECT
relname,
pg_size_pretty(
pg_table_size(relid)
) AS table_size,
pg_size_pretty(
pg_indexes_size(relid)
) AS indexes_size,
pg_size_pretty(
pg_total_relation_size(relid)
) AS total_size
FROM pg_catalog.pg_statio_user_tables
WHERE relname IN
(
'orders_bigint',
'orders_uuidv4',
'orders_uuidv7'
);
This is more useful than measuring only the primary-key index.
In a real schema, foreign keys and secondary indexes can contain copies of the identifier, so key width can influence total storage beyond the primary key itself.
Measure Physical Correlation
PostgreSQL maintains statistics that can describe correlation between column ordering and physical table order.
Check:
SELECT
tablename,
attname,
correlation
FROM pg_stats
WHERE tablename IN
(
'orders_bigint',
'orders_uuidv4',
'orders_uuidv7'
)
AND attname = 'id';
The correlation value is useful as an indicator of how closely physical row order follows index-key order.
You should not interpret one correlation number as a complete performance result.
Use it alongside:
Index size
Query plan
Buffers
Execution time
Data distribution
Inspect the Query Plan
Use:
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders_uuidv7
WHERE id = '019535d9-3df7-79fb-b466-fa907fa17f9e';
For a numeric key:
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders_bigint
WHERE id = 500000;
Look at:
Execution Time
Shared Hit Blocks
Shared Read Blocks
Index Scan
Rows Removed
Planning Time
Do not compare execution time from two queries that retrieve different amounts of data.
Test Range Queries
UUIDv7 has another interesting property.
Because it is time-ordered, PostgreSQL provides a function to extract a timestamp from UUIDv7 values.
You can inspect it:
SELECT
id,
uuid_extract_timestamp(id)
FROM orders_uuidv7
LIMIT 10;
This does not mean UUIDv7 should automatically replace a dedicated timestamp column.
For example:
id
created_at
still have different responsibilities.
The identifier provides identity.
The timestamp provides an explicit business timestamp.
Use both when the application needs both.
Range Query by UUIDv7
Because UUIDv7 is temporally ordered, an ID range can correspond approximately to a time range.
However, building application logic around UUID boundaries is often less readable than querying a dedicated timestamp column.
Prefer:
SELECT *
FROM orders_uuidv7
WHERE created_at >= '2026-08-01'
AND created_at < '2026-09-01';
rather than using UUID values as a replacement for timestamps.
UUIDv7's temporal ordering is an additional property, not a reason to remove explicit business timestamps.
Test Ordering
You can inspect the ordering directly:
SELECT id
FROM orders_uuidv7
ORDER BY id
LIMIT 20;
Then:
SELECT
id,
uuid_extract_timestamp(id)
FROM orders_uuidv7
ORDER BY id
LIMIT 20;
You should observe the relationship between UUID ordering and embedded timestamps.
PostgreSQL documents UUIDv7 as timestamp-ordered, while also noting that the timestamp contains sub-millisecond and random components.
This is why UUIDv7 is better described as time-ordered, not simply "sequential."
UUIDv7 Is Not a Perfect Sequence
A common misconception is:
UUIDv7
0001
0002
0003
0004
That is not how it works.
UUIDv7 includes timestamp information plus additional bits.
Conceptually:
UUIDv7
+------------------+------------------+
| Timestamp | Random / extra |
+------------------+------------------+
Therefore:
UUIDv7 != BIGINT sequence
It provides temporal ordering while retaining UUID characteristics.
Why UUIDv4 Can Behave Differently
UUIDv4 is intentionally random.
For a primary-key B-tree:
UUIDv4 insertion
|
+--> Existing page A
+--> Existing page B
+--> Existing page C
+--> Existing page D
A sequential key generally has a much more predictable insertion location:
Sequential key
|
v
Right side of index
UUIDv7 moves the UUID workload closer to the latter pattern because new identifiers are time ordered.
That is the hypothesis the benchmark should test.
Do not convert this into a universal claim about all PostgreSQL workloads.
Test Index Fragmentation Carefully
Index fragmentation is often discussed loosely.
If you want to investigate page-level behavior, extensions such as pageinspect can expose internal B-tree information.
For example:
CREATE EXTENSION IF NOT EXISTS pageinspect;
You can inspect metadata using PostgreSQL's page inspection functions.
However, these functions expose implementation details.
They should be used for research and diagnostics rather than becoming an application dependency.
The important benchmark question is:
Does the key strategy produce meaningfully different
index structure under the workload being tested?
Test High-Concurrency Inserts
Single-session inserts are not enough.
Use multiple clients:
Client 1 ----\
Client 2 -----\
Client 3 ------> PostgreSQL
Client 4 -----/
Client 5 ----/
Compare:
BIGINT
UUIDv4
UUIDv7
under the same concurrency level.
For example:
| Concurrency | BIGINT | UUIDv4 | UUIDv7 |
|---|
| 1 | Measure | Measure | Measure |
| 4 | Measure | Measure | Measure |
| 16 | Measure | Measure | Measure |
| 32 | Measure | Measure | Measure |
The values should come from your own test environment.
Concurrency can change the behavior significantly, so a single-thread benchmark should not be used to predict production throughput.
Use pgbench for Repeatable Workloads
pgbench is useful for PostgreSQL benchmarking because it can generate repeatable workloads and run concurrent clients.
You can build custom scripts around your test tables.
For example:
pgbench
|
+--> BIGINT workload
|
+--> UUIDv4 workload
|
+--> UUIDv7 workload
The exact benchmark script should be identical except for the target table.
Record:
TPS
Latency
Clients
Transactions
Duration
Then repeat the experiment.
Benchmark With EF Core
Database benchmarks should also consider the application's data-access layer.
For a .NET application, define three entities:
public sealed class BigIntOrder
{
public long Id { get; set; }
public long CustomerId { get; set; }
public decimal Amount { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
UUIDv4 and UUIDv7 can use the same CLR representation:
public sealed class UuidOrder
{
public Guid Id { get; set; }
public long CustomerId { get; set; }
public decimal Amount { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
The database determines whether the uuid value is generated using UUIDv4 or UUIDv7.
For PostgreSQL 18:
CREATE TABLE orders_uuidv7
(
id uuid PRIMARY KEY DEFAULT uuidv7(),
customer_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
This lets PostgreSQL remain responsible for identifier generation.
Configure EF Core
A basic PostgreSQL configuration looks like:
builder.Services.AddDbContext<AppDbContext>(
options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString(
"Postgres")));
Then configure the UUID property:
modelBuilder.Entity<UuidOrder>()
.Property(x => x.Id)
.HasDefaultValueSql("uuidv7()");
This keeps the default-generation logic in PostgreSQL.
It also means your application does not need a separate UUIDv7 generation package simply to create database-generated identifiers.
Benchmark EF Core and Raw SQL Separately
If you want to understand database behavior, first benchmark SQL directly.
Then benchmark through EF Core.
Why?
Because:
Database performance
and:
Application + ORM + database performance
are different measurements.
The layers look like:
EF Core
|
v
Npgsql
|
v
PostgreSQL
|
v
Storage
A slow benchmark could originate in any layer.
Keep the first experiment as close to PostgreSQL as practical.
Use BenchmarkDotNet for Application-Level Tests
For .NET-level measurements, BenchmarkDotNet can provide a repeatable harness.
A simplified example:
[MemoryDiagnoser]
public class IdentifierBenchmark
{
[Benchmark]
public Guid GenerateGuid()
{
return Guid.NewGuid();
}
}
However, do not use a local CPU benchmark to claim database performance.
BenchmarkDotNet is appropriate for measuring application-side operations.
For database performance, use controlled database benchmarks and capture PostgreSQL metrics separately.
Measure Storage Through Foreign Keys
Primary-key size is only part of the story.
Consider:
CREATE TABLE order_items
(
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL
);
With UUIDs:
CREATE TABLE order_items_uuid
(
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id uuid NOT NULL
);
The wider UUID also appears in:
Foreign-key indexes
Join keys
Secondary indexes
Materialized structures
Caches
Network payloads
Therefore, if storage efficiency is an important requirement, benchmark the entire schema.
Benchmark Joins
Create a child table:
CREATE INDEX ix_order_items_order_id
ON order_items_uuid(order_id);
Then test:
EXPLAIN (ANALYZE, BUFFERS)
SELECT o.id, oi.id
FROM orders_uuidv7 o
JOIN order_items_uuid oi
ON oi.order_id = o.id
WHERE o.id = '019535d9-3df7-79fb-b466-fa907fa17f9e';
Compare the same workload with numeric keys.
This is more representative of a real application than benchmarking primary-key insertion alone.
Test Bulk Inserts Separately From OLTP
There are at least two different workloads:
OLTP
----
Small transactions
Concurrent clients
Frequent commits
Bulk Load
---------
Large batches
Fewer commits
Sequential processing
A key strategy can behave differently under each.
Run separate benchmarks.
Do not use a bulk-load result to make a claim about high-concurrency OLTP behavior.
Measure Cache Effects
A query can behave differently when the relevant pages are already in memory.
For example:
First execution
|
v
Disk / shared buffers
Later execution
|
v
Shared buffers
Use:
EXPLAIN (ANALYZE, BUFFERS)
and record:
Shared Hit Blocks
Shared Read Blocks
This helps determine whether a result is primarily influenced by cache state.
Avoid Unsupported Universal Benchmarks
Do not publish:
UUIDv7 is 40% faster than BIGINT.
unless you have a reproducible methodology that supports exactly that statement.
Even then, qualify the result:
In this workload, on this PostgreSQL version,
with this hardware and concurrency level...
Database performance is highly dependent on:
Hardware
PostgreSQL configuration
Dataset size
Memory
Cache state
Index layout
Transaction size
Concurrency
Storage
Query patterns
A benchmark should expose these variables.
Example Benchmark Matrix
A useful test matrix is:
| Test | BIGINT | UUIDv4 | UUIDv7 |
|---|
| 1M sequential inserts | Measure | Measure | Measure |
| 10M inserts | Measure | Measure | Measure |
| Random point lookup | Measure | Measure | Measure |
| Range query | Measure | Measure | Measure |
| Concurrent inserts | Measure | Measure | Measure |
| Index size | Measure | Measure | Measure |
| Table size | Measure | Measure | Measure |
| Join workload | Measure | Measure | Measure |
| EF Core insert | Measure | Measure | Measure |
| EF Core lookup | Measure | Measure | Measure |
This provides a much stronger article than presenting one insert benchmark.
Benchmark Results Template
Your final results can be reported like this:
| Metric | BIGINT | UUIDv4 | UUIDv7 |
|---|
| Insert throughput | Measured | Measured | Measured |
| p50 insert latency | Measured | Measured | Measured |
| p95 insert latency | Measured | Measured | Measured |
| Primary-key index size | Measured | Measured | Measured |
| Total table size | Measured | Measured | Measured |
| Point lookup p95 | Measured | Measured | Measured |
| Range query p95 | Measured | Measured | Measured |
| Concurrent throughput | Measured | Measured | Measured |
The values should be generated by the benchmark environment.
If you are writing this as a research-style C# Corner article, publishing the test environment alongside these numbers is more valuable than presenting unsupported percentages.
Security Consideration: UUIDv7 Is Time-Ordered
UUIDv7's ordering property is useful, but it also means the identifier carries temporal information.
PostgreSQL provides uuid_extract_timestamp() specifically for extracting the timestamp component from UUID version 7 values.
That means developers should consider whether exposing a UUIDv7 identifier externally reveals information about when the identifier was generated.
For internal database keys this may be acceptable.
For public URLs, APIs, or resources where creation timing itself is sensitive, evaluate whether UUIDv7 is appropriate.
The decision is application-specific.
When BIGINT Is Still a Good Choice
UUIDv7 should not become the default simply because PostgreSQL 18 supports it.
BIGINT remains attractive when:
Single database
Centralized key generation
Compact storage
Simple ordering
Internal identifiers
are important requirements.
For example:
id bigint GENERATED ALWAYS AS IDENTITY
is straightforward and compact.
If distributed identifier generation is unnecessary, a numeric identity may still be the simpler design.
When UUIDv7 Is Attractive
UUIDv7 becomes more interesting when the application needs:
Distributed ID generation
Globally unique identifiers
Time-oriented ordering
UUID compatibility
Reduced randomness in index insertion
Application-generated IDs
It is particularly relevant to systems where identifiers need to be generated across multiple application instances or services.
PostgreSQL 18's native uuidv7() removes the need for application-side UUIDv7 generation when database-side generation fits the architecture.
When UUIDv4 Still Makes Sense
UUIDv4 remains useful when random identifiers are desirable.
For example:
Public resource IDs
Security-sensitive opaque identifiers
Systems already standardized on UUIDv4
Cross-platform UUID generation
The right identifier depends on the application's requirements.
Do not migrate an existing UUIDv4 system to UUIDv7 simply because the newer version exists.
Measure whether the change solves a real problem.
Migration Considerations
If an existing application already uses UUIDv4:
Existing:
uuid PRIMARY KEY
the column can store UUIDv7 values because PostgreSQL's uuid type supports both UUIDv4 and UUIDv7. PostgreSQL documents native generation for both versions.
But changing future inserts does not magically reorganize existing index data.
A migration strategy needs to consider:
Existing UUIDv4 rows
|
v
New UUIDv7 rows
This may temporarily produce a mixed population.
If a complete conversion is required, evaluate:
Foreign keys
Secondary indexes
Replication
Application references
API contracts
Migration downtime
Backfill strategy
Do not perform a large primary-key migration without testing the entire dependency graph.
Common Mistakes
Assuming UUIDv7 Is Sequential
It is time-ordered, not a simple incrementing sequence.
Comparing Different Workloads
If one table uses different row sizes or indexes, the result is not a key comparison.
Measuring Only Insert Speed
Storage and read patterns also matter.
Ignoring Foreign Keys
UUID width can affect related indexes and joins.
Using One Benchmark Run
A single run can be dominated by cache or system variability.
Ignoring Concurrency
High-concurrency workloads can behave differently from single-session inserts.
Treating UUIDv7 as a Timestamp
Keep an explicit created_at column when business logic needs a timestamp.
Publishing Universal Performance Claims
Results should be tied to the test environment and workload.
Ignoring Public Identifier Semantics
UUIDv7 carries temporal information.
Troubleshooting Unexpected Results
UUIDv7 Is Not Faster Than UUIDv4
That does not necessarily indicate a problem.
Check:
Dataset size
Cache state
Index size
Storage
Concurrency
Workload distribution
Query type
The benefit may appear only for particular workloads.
BIGINT Is Still Faster
That is entirely plausible.
BIGINT is smaller than UUID and provides straightforward sequential values.
UUIDv7's main value is not "beating BIGINT at everything."
Its value is combining UUID characteristics with temporal ordering.
UUIDv7 Index Is Larger Than Expected
Check:
Row count
Index definition
Other indexes
Fillfactor
Page usage
Database version
Measure the actual relation rather than estimating size from key width alone.
Point Lookups Show No Difference
That may be expected.
A primary-key equality lookup can be highly efficient for all three key types.
The differences may become more visible in insertion locality, index growth, range behavior, or high-volume workloads.
Results Change Between Runs
Check:
Shared buffers
OS cache
Background activity
Autovacuum
Checkpoints
Concurrent workloads
Dataset state
Benchmark in an isolated environment.
Best Practices
Benchmark BIGINT, UUIDv4, and UUIDv7 under the same workload.
Measure both database and application-level performance.
Report index and total storage separately.
Test concurrency rather than only single-session inserts.
Use EXPLAIN (ANALYZE, BUFFERS) for query analysis.
Measure p50, p95, and p99 latency where appropriate.
Keep an explicit timestamp column for business time.
Use UUIDv7 when distributed identity and temporal ordering are both valuable.
Keep BIGINT when its simplicity and compactness fit the architecture.
Do not migrate UUIDv4 systems without measuring the actual benefit.
Benchmark foreign-key and join workloads.
Document hardware, PostgreSQL configuration, dataset size, and concurrency.
Separate OLTP and bulk-load benchmarks.
Consider the implications of time information in public UUIDv7 identifiers.
Avoid universal performance claims based on one environment.
Conclusion
PostgreSQL 18 changes the UUID conversation by adding native UUIDv7 generation.
The database now supports:
uuidv4()
for random UUID generation and:
uuidv7()
for timestamp-ordered UUID generation.
That makes UUIDv7 an interesting option for applications that need globally unique identifiers without giving up temporal ordering.
But UUIDv7 should not be described simply as a faster replacement for BIGINT.
The three strategies solve slightly different problems:
BIGINT
|
+--> Compact
+--> Sequential
+--> Simple
UUIDv4
|
+--> Globally unique
+--> Random
+--> Distributed generation
UUIDv7
|
+--> Globally unique
+--> Time-ordered
+--> Distributed generation
The right decision depends on the workload.
If the application is a centralized system with simple numeric identifiers, BIGINT may remain the most practical choice.
If the system requires distributed UUID generation but does not need temporal ordering, UUIDv4 may still be appropriate.
If the system needs distributed identifiers while benefiting from time-oriented ordering, UUIDv7 deserves serious evaluation.
The most valuable engineering exercise is therefore not asking:
"Which identifier is fastest?"
It is asking:
"Which identifier provides the right combination of storage, locality, distribution, ordering, security, and application behavior for this workload?"
PostgreSQL 18 gives developers another strong option.
The benchmark should determine whether that option is actually valuable for your system.
Frequently Asked Questions
Is UUIDv7 faster than BIGINT?
Not universally. BIGINT is smaller and naturally sequential, so it can remain highly efficient. UUIDv7's advantage is that it combines UUID semantics with temporal ordering. Benchmark the actual workload before choosing.
Is UUIDv7 better than UUIDv4 for PostgreSQL indexes?
UUIDv7 is designed to be time-ordered, while UUIDv4 is random. This can improve insertion locality for B-tree indexes under appropriate workloads, but the magnitude of any improvement depends on the workload and database environment. PostgreSQL 18 officially describes UUIDv7 as timestamp-ordered.
Does UUIDv7 replace the created_at column?
No. UUIDv7 contains timestamp information, but an explicit created_at column is clearer for business queries, reporting, auditing, and application semantics.
Can PostgreSQL 18 generate UUIDv7 automatically?
Yes. PostgreSQL 18 provides the uuidv7() function, which can be used as a column default.
Can UUIDv4 and UUIDv7 exist in the same PostgreSQL uuid column?
Yes. The PostgreSQL uuid data type supports UUID values defined by the relevant UUID standards, and PostgreSQL 18 provides native generation functions for both UUIDv4 and UUIDv7.
Does UUIDv7 guarantee strict insertion order?
No. UUIDv7 is time-ordered, but it contains additional sub-millisecond and random information. It should not be treated as an exact replacement for an incrementing database sequence.
Should I change an existing UUIDv4 database to UUIDv7?
Not automatically. First determine whether UUIDv4 index locality or storage behavior is actually causing a measurable problem. A migration also affects foreign keys, indexes, replication, APIs, and application code.
Is UUIDv7 safe for public URLs?
It can be, but evaluate whether exposing temporal information through identifiers is acceptable for your application. PostgreSQL provides uuid_extract_timestamp() for UUIDv7, demonstrating that the timestamp component is recoverable.