Skip to content

UUID Variants

James Brucker edited this page Jun 13, 2025 · 10 revisions

UUID are globally unique identifiers that are 16-bytes long. Downsides of UUID are:

  • database indices are fragmented
  • not chronologically ordered
  • use 16 bytes compared to 4 bytes for SERIAL, INTEGER, or GENERATED AS IDENTITY

Several variants exist: UUIDv1, UUIDv7, and ULID.

Below is a comparison of 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 bit timestamp + 48 bit node id + 14 bit sequence 48 bit timestamp + 74 bit random + 6 bit version/variant 48 bit timestamp + 80 bit random
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, almost surely Yes, almost surely
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)
Postgres Support Native uuid-ossp PG pg_uuidv7 extension 3rd Party Extension pg_ulid
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

UUIDv1

  • 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)

UUIDv7 (Time-Ordered UUID)

  • 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)

ULID (Universally Unique Lexicographically Sortable Identifier)

  • 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

When to Choose Which?

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

Practical Tip

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.

Postgres Support for UUIDv7

Postgres has a built-in extension for UUIDv7:

-- Install extension (requires PostgreSQL 16+ or pg_uuidv7 extension)
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;

-- Table definition
CREATE TABLE readings (
    id UUID DEFAULT uuid_generate_v7() PRIMARY KEY,
    source_id UUID NOT NULL,
    value FLOAT NOT NULL,
    timestamp TIMESTAMPTZ DEFAULT NOW()
);

-- Index for time-sorted queries
CREATE INDEX idx_readings_id ON readings (id);  -- Sorted by time implicitly

Note: the comment is inconsistent with early code that claimed UUIDv7 supported in PostgreSQL 14+.

Postgres Support for ULID

ULID is not a built-in type in PostgreSQL. There are several ways to implement it in PostgreSQL:

  1. pg_ulid (or similar) Postgres extension adds a true ULID column type and generator functions (ulid_generate() and gen_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(),
  2. 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
  3. Store ULIDs in the UUID Type In Database:

    CREATE TABLE users {
       id     UUID PRIMARY KEY  DEFAULT uuid_nil(),   -- placeholder

In Python using the psycopg2 extension, generate a ULID, convert it to a uuid.UUID, and insert into a new user row in database:

# Requires extra package for "ulid"
import ulid
import uuid
import psycopg2
from psycopg2.extras import register_uuid

# 1) Generate a new ULID
new_ulid = ulid.new()      # WRONG!  Should be:  ulid.ULID()

# 2) Convert ULID bytes to UUID
#    This treats the 128-bit ULID payload as a UUID.
new_uuid = uuid.UUID(bytes=new_ulid.bytes)

# 3) (Optional) Register UUID adapter so psycopg2 knows how to send uuid.UUID
register_uuid()

# 4) Connect to your Postgres database
conn = psycopg2.connect(
    dbname="your_db",
    user="your_user",
    password="your_password",
    host="your_host",
    port=5432,
)
cur = conn.cursor()

# 5) Insert the new user
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.

Which Python Package for ULID Support?

ChatGPT and Deepseek disagree. One of these:

  • ulid-py. pip install ulid
  • ulid2 (More actively maintained, additional functions)
  • python-ulid Deepseek suggested this. Not sure.

Clone this wiki locally