-
Notifications
You must be signed in to change notification settings - Fork 0
UUID Variants
UUID are globally unique identifiers that are 16-bytes long. Downsides of UUID are:
- database indices are fragmented
- not chronologically ordered
Several variants exist: UUIDv1, UUIDv7, and ULID.
Below is a comparison highlighting their structure, properties, privacy considerations, and typical use-cases.
| Feature | UUIDv1 | UUIDv7 | ULID |
|---|---|---|---|
| Specification | RFC 4122 | IETF draft (UUIDv7, time-ordered) | ULID spec |
| Bit Layout | 60 bits timestamp + 48 bits node + 14 bits sequence | 48 bits timestamp + 74 bits randomness + 6 bits version/variant | 48 bits timestamp + 80 bits randomness |
| Timestamp Resolution | 100 ns increments since 1582-10-15 | 1 ms since Unix epoch (1970-01-01) | 1 ms since Unix epoch |
| Lexicographic Order | Yes, if compared as raw bytes | Yes, natural time ordering | Yes, sorts by timestamp then random |
| Global Uniqueness | Yes (node/MAC + sequence) | Yes (timestamp + random) | Yes (timestamp + random) |
| Privacy | Leaks MAC or machine identifier | No machine identifier; only time + random | No machine identifier; only time + random |
| Spoof Resistance | Weak (MAC can be spoofed; collisions if clock moves backward) | Stronger (random cushion; monotonic if implemented carefully) | Strong (monotonic cushion; collision risk only within same ms and same monotonic sequence) |
| Size on Disk/Index | 16 bytes | 16 bytes | 16 bytes |
| Ease of Generation | Widely supported in standard libraries | Emerging support; requires up-to-date libraries | Widely supported (multiple languages) |
| Use-Cases | Legacy systems; when backward compatibility with UUIDv1 is required | New applications needing RFC-style UUIDs with time order | Time-sortable IDs with simple spec; especially JS/browser use |
- Structure
- 60 bits of a 100-nanosecond timestamp (since 1582-10-15)
- 14 bit sequence (to avoid collisions within the same timestamp)
- 48 bit “node” (MAC address or random fallback)
- Pros
- Naturally ordered by generation time
- Supported everywhere (standard in most UUID libraries)
- Cons
- Reveals hardware/MAC address (privacy leak)
- Vulnerable to clock regression (requires sequence bump)
- Structure
- 48 bit Unix-millisecond timestamp
- 74 bit cryptographically random payload
- 6 bit version/variant markers
- Pros
- Millisecond ordering of IDs (good for time-series indexing)
- No hardware identifier
- Fully compliant with the overall UUID format
- Cons
- Library support is still emerging (you may need a custom generator)
- Less granular timestamp (millisecond vs. 100 ns)
- Structure
- 48 bit Unix-millisecond timestamp
- 80 bit cryptographically random payload
- Pros
- Lexicographically sortable when encoded as Crockford’s Base32 (26 chars)
- No hardware identifier, simpler spec than UUIDv7
- Built-in monotonicity (“monotonic ULID”) to avoid collisions within the same ms
- Cons
- Not a UUID; may not integrate with libraries expecting RFC-4122 format
- Slightly larger random payload than UUIDv7 (80 bits vs. 74 bits)
-
UUIDv1: Legacy compatibility or when library support and fine-grained (100 ns) timestamps matter—and you can tolerate the MAC leak (or use a random node).
-
UUIDv7: If you want to stick with the UUID standard, need lexicographic time ordering, and don’t mind millisecond resolution (and you have a library that supports v7).
-
ULID: When you need simple, time-sortable IDs in environments like JavaScript/browser, prefer a compact Base32 string, and can adopt a non-UUID identifier.
If you’re storing and indexing large, time-ordered logs (e.g. meter readings), time-ordered IDs (UUIDv7 or ULID) help keep your B-tree indexes hot at the “right” end—minimizing page splits and improving insert throughput. For most new projects, ULID is often the easiest to adopt; if you need strict UUID compatibility, go with UUIDv7.
ULID is not a built-in type in PostgreSQL. There are several ways to implement it in PostgreSQL:
-
pg_ulid(or similar) Postgres extension adds a true ULID column type and generator functions (ulid_generate()andgen_ulid()):-- As superuser install the extension in your database CREATE EXTENSION IF NOT EXISTS pg_ulid; -- Usage in a table CREATE TABLE users { id ulid PRIMARY KEY DEFAULT ulid_generate(),
-
Store ULIDs as a Fixed-Length String.
In Database:CREATE TABLE users { id CHAR(26) PRIMARY KEY,
in FastAPI:
user_id = str(ulid.new()) # pass user_id to INSERT commands for users
-
Store ULIDs in the UUID Type In Database:
CREATE TABLE users { id UUID PRIMARY KEY DEFAULT uuid_nil(), -- placeholder
in Python using the
psycopg2extension, generate a ULID, convert it to a uuid.UUID, and insert into a new user row in database:
import ulid import uuid import psycopg2 from psycopg2.extras import register_uuid
new_ulid = ulid.new()
new_uuid = uuid.UUID(bytes=new_ulid.bytes)
register_uuid()
conn = psycopg2.connect( dbname="your_db", user="your_user", password="your_password", host="your_host", port=5432, ) cur = conn.cursor()
cur.execute( """ INSERT INTO users (id, email, display_name, created_at) VALUES (%s, %s, %s, NOW()) """, ( new_uuid, # will be sent as PostgreSQL UUID "alice@example.com", "Alice Doe", ), )
conn.commit() cur.close() conn.close()
### Which approach to choose?
- For simplicity and full ULID semantics inside the database, install a ULID extension (`pg_ulid`).
- For tight control or if you can’t install extensions on a managed service, store ULIDs as CHAR(26) and generate them in FastAPI.
- If you already rely heavily on UUID and want to reuse indexes and tooling, pack ULIDs into UUID columns with application-level conversion.
All three approaches preserve lexicographic sort-order by timestamp and global uniqueness.
For a managed Supabase deployment, you can either install the extension via their SQL editor (if they support it) or default to the CHAR(26) column strategy.