ULID vs UUID v7: identificadores ordenados por tempo para bancos de dados

1. Introdução aos identificadores ordenáveis por tempo

Identificadores únicos universais (UUIDs) são onipresentes em sistemas modernos, mas a escolha do formato impacta diretamente a performance de bancos de dados. O UUID v4, amplamente utilizado, gera valores aleatórios que causam fragmentação severa em índices B-Tree — a estrutura de dados padrão da maioria dos bancos relacionais. Quando milhares de registros são inseridos por segundo, cada novo UUID v4 é inserido em uma posição aleatória da árvore, forçando rebalanceamentos frequentes e degradando o desempenho de escrita.

A ordenação temporal resolve esse problema: se os identificadores forem monotonicamente crescentes no tempo, os novos registros são inseridos no final do índice, eliminando a fragmentação. Duas soluções modernas atendem a esse requisito: ULID (Universal Unique Lexicographically Sortable Identifier) e UUID v7 (definido pela RFC 9562). Ambas incorporam timestamps em sua estrutura, mas diferem em formato, implementação e casos de uso ideais.

2. Anatomia do ULID: estrutura e propriedades

O ULID é um identificador de 26 caracteres codificado em base32 Crockford, dividido em duas partes:

  • Timestamp (10 caracteres): 48 bits representando milissegundos desde a época Unix (1970-01-01). Isso permite ordenação lexicográfica precisa — um ULID gerado antes sempre será "menor" que um gerado depois.
  • Componente aleatório (16 caracteres): 80 bits de entropia, fornecendo 1.21e+24 identificadores únicos por milissegundo.

Exemplo de ULID:

01ARZ3NDEKTSV4RRFFQ69G5FAV

A ordenação lexicográfica funciona porque o timestamp ocupa os primeiros 10 caracteres. Em uma string, "01ARZ3NDEK" (timestamp mais antigo) é lexicograficamente menor que "01ARZ3NDEL" (timestamp mais recente). Isso permite ordenação direta como string, sem necessidade de conversão.

O ULID é gerado puramente no lado da aplicação, sem dependência de banco de dados. Bibliotecas populares existem para Node.js (ulid), Python (python-ulid) e Go (oklog/ulid).

3. Anatomia do UUID v7: estrutura e propriedades

O UUID v7, padronizado pela RFC 9562 em maio de 2024, é um formato de 128 bits com layout específico:

ttttttttttttttttmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm

Onde:
- 48 bits de timestamp Unix (milissegundos) — ocupando os bits mais significativos
- 74 bits aleatórios — incluindo 2 bits de variante e 4 bits de versão
- Formato de saída: 36 caracteres hexadecimais com hífens (8-4-4-4-12)

Exemplo de UUID v7:

018f3a6b-7c8d-4e5f-a1b2-c3d4e5f6a7b8

A grande vantagem do UUID v7 é o suporte nativo em bancos de dados modernos. PostgreSQL 16+ oferece gen_random_uuid() que pode gerar UUID v7 com a extensão pg_uuidv7. MySQL 9.0 introduziu UUID_TO_BIN() com suporte a ordenação temporal. SQLite e Microsoft SQL Server também estão adicionando suporte.

4. Comparação técnica: ULID vs UUID v7

Formato e legibilidade

Característica ULID UUID v7
Tamanho (string) 26 caracteres 36 caracteres
Codificação Base32 Crockford Hexadecimal
Hífens Não Sim (padrão)
Case-insensitive Sim Não (hex maiúsculo/minúsculo)

Performance de geração

Ambos os formatos são extremamente rápidos, mas o ULID tende a ser ligeiramente mais rápido em implementações otimizadas devido à codificação base32 mais simples. Em testes de benchmark com Go:

ULID:    ~180 ns/op
UUID v7: ~220 ns/op (com geração criptográfica segura)

Ordenação em índices

O ULID ordena como string — o banco compara os 26 caracteres sequencialmente. O UUID v7, quando armazenado como BINARY(16), ordena pelo valor numérico de 128 bits. Ambos funcionam bem, mas o UUID v7 como binário ocupa menos espaço:

ULID como TEXT:    26 bytes
UUID v7 como BINARY: 16 bytes

Compactação e armazenamento

Em uma tabela com 10 milhões de registros e índice primário:

ULID (TEXT):  ~260 MB de dados de índice
UUID v7 (BINARY): ~160 MB de dados de índice

5. Casos de uso e trade-offs práticos

Sistemas distribuídos

O ULID é ideal quando você precisa de identificadores ordenáveis em sistemas sem suporte nativo a UUID v7. Por exemplo, em microsserviços que usam MongoDB ou Redis como armazenamento primário:

// Geração de ULID em Node.js
const ulid = require('ulid');
const id = ulid.ulid(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"

Bancos de dados relacionais modernos

O UUID v7 elimina a necessidade de funções customizadas. No PostgreSQL:

-- PostgreSQL com extensão pg_uuidv7
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;
CREATE TABLE pedidos (
    id UUID DEFAULT uuid_generate_v7() PRIMARY KEY,
    data_criacao TIMESTAMPTZ DEFAULT NOW()
);

-- Consultas ordenadas por tempo sem ORDER BY explícito
SELECT * FROM pedidos WHERE id > '018f3a6b-7c8d-4e5f-a1b2-c3d4e5f6a7b8';

Aplicações legadas e migrações

Migrar de UUID v4 para ULID ou UUID v7 requer reparticionamento de tabelas. A estratégia recomendada é:
1. Adicionar nova coluna com o novo formato
2. Preencher registros existentes com valores retroativos (timestamp da criação original)
3. Criar índice na nova coluna
4. Gradualmente migrar as consultas

Segurança e previsibilidade

Identificadores ordenados por tempo são previsíveis — um atacante pode estimar quando um registro foi criado. Para mitigar:
- ULID: usar entropia adicional (mais bits aleatórios)
- UUID v7: a RFC permite substituir bits aleatórios por contadores monotônicos, mas isso reduz entropia

6. Implementação e integração em projetos reais

Geração de ULID em Python

import ulid
id = ulid.new()  # "01ARZ3NDEKTSV4RRFFQ69G5FAV"

Geração de UUID v7 em Go

import "github.com/google/uuid"
id := uuid.Must(uuid.NewV7())  // "018f3a6b-7c8d-4e5f-a1b2-c3d4e5f6a7b8"

Armazenamento otimizado no MySQL

-- MySQL 9.0 com suporte nativo
CREATE TABLE eventos (
    id BINARY(16) DEFAULT (UUID_TO_BIN(UUID(), TRUE)) PRIMARY KEY,
    payload JSON
);

-- Consulta ordenada por tempo
SELECT BIN_TO_UUID(id) FROM eventos ORDER BY id;

Indexação em PostgreSQL

-- Índice B-Tree funciona naturalmente com UUID v7
CREATE INDEX idx_pedidos_tempo ON pedidos (id);

-- Consulta paginada por tempo
SELECT * FROM pedidos 
WHERE id > '018f3a6b-7c8d-4e5f-a1b2-c3d4e5f6a7b8'
ORDER BY id
LIMIT 100;

7. Considerações finais e recomendações

Quando escolher ULID

  • Sistemas legados que já usam strings como chaves
  • Bancos NoSQL (MongoDB, Cassandra) onde ordenação lexicográfica é nativa
  • Necessidade de IDs curtos e legíveis para logs ou URLs
  • Ambientes sem suporte a UUID v7 no banco de dados

Quando escolher UUID v7

  • Projetos novos em bancos relacionais modernos (PostgreSQL 16+, MySQL 9.0+)
  • Necessidade de padronização e interoperabilidade entre sistemas
  • Preocupação com espaço de armazenamento (16 bytes vs 26 bytes)
  • ORMs modernos que já oferecem suporte nativo (Prisma, TypeORM, SQLAlchemy)

Checklist de decisão (5 perguntas)

  1. O banco suporta UUID v7 nativamente? Sim → UUID v7; Não → ULID
  2. Precisa de IDs legíveis para humanos? Sim → ULID; Não → UUID v7
  3. O espaço de armazenamento é crítico? Sim → UUID v7 (16 bytes); Não → ULID
  4. Sistema é distribuído sem relógio sincronizado? Considere Snowflake ou KSUID como alternativas
  5. Precisa de máxima entropia para segurança? Ambos são equivalentes; adicione entropia extra se necessário

Tendências futuras

A indústria está convergindo para UUID v7 como padrão. A RFC 9562 já é adotada por PostgreSQL, MySQL e SQLite. ORMs como Prisma 5+ e TypeORM 0.3+ oferecem suporte nativo. O ULID permanece relevante para nichos específicos, especialmente em ecossistemas JavaScript e Go.

Referências