PostgreSQL  

Benchmarking UUIDv7 vs Sequential Keys in PostgreSQL 18

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:

  • Insert throughput

  • Primary-key index growth

  • Index size

  • Heap correlation

  • Random versus sequential access

  • Range scans

  • Concurrent inserts

  • Storage overhead

  • Application-level identifier requirements

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.

PropertyBIGINT IdentityUUIDv4UUIDv7
Size8 bytes16 bytes16 bytes
Globally generatedDatabase sequenceYesYes
Random orderingNoYesNo, time-ordered
Distributed generationRequires designYesYes
Native PostgreSQL generationYesYesYes
Time informationNoNoEmbedded
Natural numeric orderingYesNoTemporal ordering
Suitable for distributed systemsDependsStrongStrong

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:

ConcurrencyBIGINTUUIDv4UUIDv7
1MeasureMeasureMeasure
4MeasureMeasureMeasure
16MeasureMeasureMeasure
32MeasureMeasureMeasure

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:

TestBIGINTUUIDv4UUIDv7
1M sequential insertsMeasureMeasureMeasure
10M insertsMeasureMeasureMeasure
Random point lookupMeasureMeasureMeasure
Range queryMeasureMeasureMeasure
Concurrent insertsMeasureMeasureMeasure
Index sizeMeasureMeasureMeasure
Table sizeMeasureMeasureMeasure
Join workloadMeasureMeasureMeasure
EF Core insertMeasureMeasureMeasure
EF Core lookupMeasureMeasureMeasure

This provides a much stronger article than presenting one insert benchmark.

Benchmark Results Template

Your final results can be reported like this:

MetricBIGINTUUIDv4UUIDv7
Insert throughputMeasuredMeasuredMeasured
p50 insert latencyMeasuredMeasuredMeasured
p95 insert latencyMeasuredMeasuredMeasured
Primary-key index sizeMeasuredMeasuredMeasured
Total table sizeMeasuredMeasuredMeasured
Point lookup p95MeasuredMeasuredMeasured
Range query p95MeasuredMeasuredMeasured
Concurrent throughputMeasuredMeasuredMeasured

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

  1. Benchmark BIGINT, UUIDv4, and UUIDv7 under the same workload.

  2. Measure both database and application-level performance.

  3. Report index and total storage separately.

  4. Test concurrency rather than only single-session inserts.

  5. Use EXPLAIN (ANALYZE, BUFFERS) for query analysis.

  6. Measure p50, p95, and p99 latency where appropriate.

  7. Keep an explicit timestamp column for business time.

  8. Use UUIDv7 when distributed identity and temporal ordering are both valuable.

  9. Keep BIGINT when its simplicity and compactness fit the architecture.

  10. Do not migrate UUIDv4 systems without measuring the actual benefit.

  11. Benchmark foreign-key and join workloads.

  12. Document hardware, PostgreSQL configuration, dataset size, and concurrency.

  13. Separate OLTP and bulk-load benchmarks.

  14. Consider the implications of time information in public UUIDv7 identifiers.

  15. 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.