Introduction
Python 3.14 and PostgreSQL 18, both released as stable in recent months, introduced first-class support for UUIDv7, which provides time ordering and better index locality.
In this article I walk through a practical example showing how UUIDv7 works in Python first and then in PostgreSQL using Django ORM and migrations only. No manual SQL is needed and every step shows real code and migration output.
This article assumes that you are running Python 3.14 and PostgreSQL 18, since earlier versions do not provide UUIDv7 generation or timestamp extraction functions.
What is a UUID
A UUID is a 128 bit identifier that aims to be globally unique without coordination. It works well in distributed systems because it removes the need for a central sequence generator. This makes UUIDs ideal for many modern architectures.
UUIDv4 vs UUIDv7
The most common version is UUIDv4 which is fully random. UUIDv7 improves on this by adding a timestamp prefix that makes identifiers sort in creation order. This helps index locality and makes inserts more efficient.
UUIDv4
- Fully random
- Very good uniqueness
- Poor index locality in large tables
- Inserts become random writes
UUIDv7
- Timestamp based and time ordered
- Better index locality and more predictable writes
- Creation time can be extracted directly
- Suitable as a primary key in high write environments
Why ordering matters
UUIDv7 encodes a timestamp in its most significant bits, which means new identifiers follow a natural chronological order. This reduces index fragmentation and avoids the random write patterns typical of UUIDv4.
Because inserts land in predictable index regions instead of scattered locations, write performance improves significantly on large tables and high-throughput systems.
When to choose database generated UUIDv7
In real deployments, database generated UUIDv7 is usually the safest and most consistent option. PostgreSQL 18 provides a monotonic, timestamp-aware generator that avoids clock drift between hosts, guarantees stable ordering under concurrency and ensures that creation times always reflect the database’s timeline rather than the application server.
Python-side UUIDv7 generation is universal and works on all databases, but it shifts responsibility to the application layer and depends on the local system clock. It remains the only option for engines that cannot generate UUIDv7 internally, although MariaDB 11.7 has added support.
Choose database generated UUIDv7 when using PostgreSQL 18 or MariaDB 11.7. Choose Python-generated UUIDv7 when using SQLite, MySQL, Oracle or earlier versions of MariaDB and PostgreSQL.
What about SQLite, MySQL, MariaDB, Oracle?
Not all databases provide native support for UUIDv7. This is important to understand because it determines whether UUIDv7 should be generated in Python or by the database itself.
MariaDB
MariaDB historically supported only UUIDv1 generation, however, starting with MariaDB 11.7, support for UUIDv4 and UUIDv7 has been introduced, but timestamp extraction is not supported.
MySQL
MySQL provides UUIDv1 function but does not currently support UUIDv4 or UUIDv7. When using MySQL, UUIDv7 must be generated at the application layer (e.g. Python uuid.uuid7()).
Oracle
Oracle supports only UUIDv4, starting with Oracle 23ai, but it does not provide a native UUIDv7 generator or timestamp extraction functions. As with other databases lacking UUIDv7 support, UUIDv7 must be generated in Python when using Oracle.
PostgreSQL
PostgreSQL 18 provides native uuidv7() generation and uuid_extract_timestamp(), but it also support UUIDv4 since version 13.
SQLite
SQLite can store UUIDv7 values without issues, but it cannot generate them or extract their timestamp. When using SQLite, UUIDv7 must always be generated in Python using uuid.uuid7().
Because of these differences, Python-side UUIDv7 generation is the most portable option, while database-side generation is available only when using PostgreSQL 18 or MariaDB 11.7+.
Preparing the environment
Python 3.14 is assumed to be available. The first step is creating a virtual environment and installing Django and Black, which will prepare the project structure and dependencies needed for the tutorial.
Creating the virtual environment
This step creates a clean virtual environment with Python 3.14 and installs the tools needed to start the Django project.
$ python3.14 -m venv .venv
$ source .venv/bin/activate
$ python -m pip install django black
Creating a Django project and the first UUIDv7 model
The next step is creating the Django project and application that will contain the model used in the UUIDv7 example.
Creating the Django project and application
Here we start a new Django project named uuidv7 and add an items app that will contain the models used in the examples.
$ python -m django startproject uuidv7
$ tree --noreport uuidv7/
uuidv7/
├── manage.py
└── uuidv7
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
$ cd uuidv7
$ python -m django startapp items
$ tree --noreport items/
items/
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
Enable the app in settings:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"items",
]
Creating the Item model using Python uuid.uuid7
This model uses Python 3.14 builtin uuid.uuid7 for primary key generation and adds a cached_property to extract the timestamp from the UUID. It demonstrates client side UUIDv7 usage without depending on the database.
import uuid
from datetime import datetime
from django.db import models
from django.utils.functional import (
cached_property,
)
class Item(models.Model):
uuid = models.UUIDField(
default=uuid.uuid7, primary_key=True
)
@cached_property
def creation_time(self):
return datetime.fromtimestamp(
self.uuid.time / 1000
)
Creating the initial migration
This section generates the migration file that defines the database table for the Item model. The migration uses SQLite as the default backend and produces a straightforward schema containing only the UUID column.
$ python -m manage makemigrations
Migrations for 'items':
items/migrations/0001_initial.py
+ Create model Item
Inspecting the SQL for SQLite
This command shows the SQL that Django will run on SQLite, making it easier to understand how the Item model is represented in the database.
$ python -m manage sqlmigrate items 0001
BEGIN;
--
-- Create model Item
--
CREATE TABLE "items_item" (
"uuid" char(32) NOT NULL PRIMARY KEY
);
COMMIT;
Applying the migration
Now we apply the migration so the Item table is created in the local SQLite development database.
$ python -m manage migrate items 0001
Operations to perform:
Target specific migration: 0001_initial, from items
Running migrations:
Applying items.0001_initial... OK
Testing the Item model in Django shell
This example creates an Item instance and reads the timestamp encoded in its UUIDv7 value.
>>> item = Item()
>>> item.creation_time.isoformat()
'2025-11-14T16:52:55.879000'
Using UUIDv7 in PostgreSQL 18 with Django 5.2
PostgreSQL 18 adds native support for UUIDv7 and timestamp extraction, making it possible to generate the UUID directly in the database. This section shows how to configure Django to use PostgreSQL and how to create models that rely on these functions.
Installing psycopg
We install psycopg, the PostgreSQL driver used by Django, so the project can connect to a PostgreSQL 18 database.
$ python -m pip install psycopg[binary]
Configuring PostgreSQL in settings.py
The database configuration is updated to point Django to a PostgreSQL 18 instance instead of SQLite.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": "postgres",
"NAME": "uuidv7",
"PASSWORD": "postgres",
"PORT": "5432",
"USER": "postgres",
}
}
Creating the Record model with PostgreSQL generated UUIDv7
The following model uses Django db_default to call the uuidv7 function directly inside PostgreSQL. It results in the UUID being generated in the database during the insert operation.
Defining a Django database function for uuidv7
This Django function wrapper maps directly to PostgreSQL’s uuidv7 function so it can be used in model definitions.
class UUIDv7(models.Func):
function = "uuidv7"
output_field = models.UUIDField()
Creating the Record model
The Record model uses PostgreSQL uuidv7 to generate the primary key at insert time.
class Record(models.Model):
uuid = models.UUIDField(
db_default=UUIDv7(), primary_key=True
)
Creating the migration for the Record model
We create the migration that defines the database table storing UUIDv7 values generated by PostgreSQL.
$ python -m manage makemigrations
Migrations for 'items':
items/migrations/0002_record.py
+ Create model Record
Inspecting the migration SQL for PostgreSQL
This command shows the SQL generated by Django for the Record model when using PostgreSQL.
$ python -m manage sqlmigrate items 0002
BEGIN;
--
-- Create model Record
--
CREATE TABLE "items_record" (
"uuid" uuid DEFAULT (uuidv7()) NOT NULL PRIMARY KEY
);
COMMIT;
Applying the migration
Applying this migration creates the items_record table in PostgreSQL using the uuidv7 default.
$ python -m manage migrate items 0002
Operations to perform:
Target specific migration: 0002_record, from items
Running migrations:
Applying items.0001_initial... OK
Applying items.0002_record... OK
Testing the Record model
Here we create a new Record and verify that PostgreSQL generated a UUIDv7 value.
>>> record = Record.objects.create()
>>> record.uuid
UUID('019a8359-f49d-7f56-8921-977e2c47242c')
PostgreSQL 18 offers uuid_extract_timestamp which can extract the timestamp encoded inside a UUIDv7. This section shows how to expose that value in Django using a GeneratedField.
This function wrapper exposes PostgreSQL’s uuid_extract_timestamp so it can be used in a GeneratedField.
class UUIDExtractTimestamp(models.Func):
function = "uuid_extract_timestamp"
output_field = models.DateTimeField()
Adding the creation_time generated column
The Record model is extended with a generated column that stores the timestamp extracted from the UUID.
class Record(models.Model):
uuid = models.UUIDField(
db_default=UUIDv7(), primary_key=True
)
creation_time = models.GeneratedField(
expression=UUIDExtractTimestamp("uuid"),
output_field=models.DateTimeField(),
db_persist=True,
)
Generating the migration for creation_time
This migration adds the creation_time column and configures PostgreSQL to compute it automatically.
$ python -m manage makemigrations
Migrations for 'items':
items/migrations/0003_record_creation_time.py
+ Add field creation_time to record
Inspecting the SQL for the generated column
This shows the SQL statement that defines the generated timestamp column in PostgreSQL.
$ python -m manage sqlmigrate items 0003
BEGIN;
--
-- Add field creation_time to record
--
ALTER TABLE "items_record"
ADD COLUMN "creation_time" timestamp with time zone
GENERATED ALWAYS AS (uuid_extract_timestamp("uuid")) STORED;
COMMIT;
Applying the migration
Running this migration creates the generated column and enables timestamp extraction for new rows.
$ python -m manage migrate items 0003
Operations to perform:
Target specific migration: 0003_record_creation_time, from items
Running migrations:
Applying items.0003_record_creation_time... OK
This final example inserts a new Record and checks the extracted timestamp stored in the generated column.
>>> record = Record.objects.create()
>>> record.creation_time.isoformat()
'2025-11-14T17:18:24.144000+00:00'
A generated datetime column can be very useful even though UUIDv7 already embeds a timestamp. PostgreSQL computes it at write time, Django manages it declaratively through the ORM and having a proper datetime field makes filtering, ordering, indexing and using the Django admin much simpler without requiring annotations or extra computation on the UUID value.
Summary
Use Python 3.14 uuid.uuid7 when:
- identifiers must be generated inside application code
- you are using databases that cannot generate UUIDv7 natively (such as SQLite, Oracle, MySQL or MariaDB)
- deterministic local generation is useful
Use PostgreSQL 18uuidv7 when:
- the database is responsible for generating identifiers
- multiple writers insert rows concurrently
- you want to expose creation time using uuid_extract_timestamp
- sequential inserts improve performance
These features require PostgreSQL 18’s native functions. SQLite and other engines cannot generate UUIDv7 or extract timestamps at the database level.
Both methods are valid and can be mixed in the same project.
Key takeaways
- UUIDv7 provides ordered identifiers that improve index locality and reduce random writes compared to UUIDv4.
- Python 3.14 can generate UUIDv7 directly with
uuid.uuid7, making it easy to use even without database support. - PostgreSQL 18 adds native
uuidv7anduuid_extract_timestamp, allowing server side UUID generation and timestamp extraction without storing extra fields. - Django 5.2 integrates smoothly with both Python and PostgreSQL UUIDv7 through
db_defaultandGeneratedField. - UUIDv7 should not be exposed directly in public APIs because the embedded timestamp can reveal creation patterns or activity timing.
- UUIDv47 offers a reversible masking approach that keeps UUIDv7 internally while exposing a UUIDv4 looking value externally.
Considering the downsides of UUIDv7 and possible solutions
Drawbacks of exposing UUIDv7 to external users
UUIDv7 works very well inside a database, but it is not always ideal as a public identifier. Because the most significant bits contain a precise Unix timestamp, anyone who sees the UUID can infer when the record was created. This can reveal activity patterns, account creation times or traffic trends, which in some contexts may help correlation or de anonymization attacks. The random portion of the UUID remains intact, but the timestamp still leaks meaningful metadata. For these reasons UUIDv7 is generally recommended for internal use, while external facing APIs should avoid exposing it directly.
Masking UUIDv7 timestamps with UUIDv47
A practical solution to these issues is provided by the UUIDv47 approach. The idea is to keep a true UUIDv7 inside the database so that ordering and index locality are preserved, but to expose a masked representation externally. UUIDv47 applies a reversible SipHash based transformation to the timestamp field only, using a key derived from the random portion of the UUID. The result looks like a UUIDv4 to clients and hides the timing information, while still mapping deterministically and invertibly back to the original internal UUIDv7.
The project implementing this idea is available here: uuidv47
Django support through django uuid47
For Django users there is a dedicated integration package that implements UUIDv47 as a model field and handles configuration of the masking key. This allows applications to adopt masked UUIDv7 identifiers without altering the rest of the schema or the way models are defined.
The Django integration package is available here: django-uuid47
Further reading
This article builds on Django GeneratedField which I have covered in a dedicated series on my blog:
- “Database generated columns⁽¹⁾: Django SQLite”
- “Database generated columns⁽²⁾: Django PostgreSQL”
- “Database generated columns⁽³⁾: GeoDjango PostGIS”
Frequently asked questions
- Is UUIDv7 stable
- Yes. It is part of the updated UUID specification and fully implemented in Python 3.14 and PostgreSQL 18.
- Does Django support UUIDv7
- Yes. Django works with UUIDv7 without custom fields because Python and PostgreSQL provide the required functions.
- Can UUIDv7 be used as a primary key
- Yes. It offers far better index locality than UUIDv4 and is well suited to primary keys in most applications.
- Should I migrate existing data from UUIDv4 to UUIDv7
- This depends on your system. New projects can adopt UUIDv7 immediately while migrations require careful consideration.
- Does UUIDv7 require PostgreSQL 18
- Yes. Native support is available starting with PostgreSQL 18.
- Do I need a custom Django field to use UUIDv7
- No. UUIDField works correctly with both Python and PostgreSQL generated UUIDv7 values.
- Is it safe to expose raw UUIDv7 values in public APIs
- Not always. UUIDv7 encodes a timestamp in its most significant bits and this can reveal when a record was created. For applications where identifiers are visible to untrusted users it is safer to use a masking scheme like UUIDv47 or expose a separate UUIDv4 instead.
- Does the timestamp inside UUIDv7 make separate datetime fields unnecessary
- It depends on the use case. The embedded timestamp is enough for ordering, but a dedicated datetime column generated by PostgreSQL is often easier to query, index, filter or use in the admin, and Django can manage it declaratively through a GeneratedField without triggers or extra query computation.
- Can SQLite/Oracle/MariaDB/MySQL generate UUIDv7
- No. SQLite/Oracle/MariaDB/MySQL can store UUIDv7 but cannot generate it or extract its timestamp. Use Python 3.14
uuid.uuid7()when working with them.
Conclusions
UUIDv7 offers ordering, predictable inserts and timestamp extraction without losing UUID benefits. With Python 3.14, Django 5.2 and PostgreSQL 18 it can be used today without custom extensions or complex setup.
Discuss this article
If you want to comment or share feedback you can do so on: