UUID vs SERIAL vs ULID no PostgreSQL: escolher e usar com Prisma
Por John Pires · 11 de março de 2026

UUID vs SERIAL vs ULID no PostgreSQL: escolher e usar com Prisma
Por que aprender isso em 2026
Em 2026, aplicações distribuídas, microserviços e requisitos de escalabilidade continuam dominando projetos reais. Escolher o tipo de identificador primário certo para suas entidades afeta migrações, performance de consultas, replicação, troubleshooting e UX dos times. Dados consistentes, previsíveis e fáceis de debugar são resultado de decisões simples: usar SERIAL/IDENTITY, UUID ou ULID. Cada opção tem vantagens práticas: IDs autoincrementais são compactos e simples; UUIDs oferecem unicidade sem coordenação; ULIDs combinam unicidade com ordenação temporal legível.
Este artigo mostra o que cada opção realmente faz, como isso impacta Postgres e Prisma, exemplos práticos, armadilhas comuns e um mini-projeto passo a passo. Não haverá floreios: foco em comandos e código que você pode rodar hoje.
O que você precisa saber antes
Você precisa entender apenas o básico: instalar PostgreSQL, Node.js e o Prisma CLI. O conteúdo explica os comandos exatos e a progressão do Hello World para um mini-projeto CRUD que compara as três estratégias de ID.
Resumo rápido das opções
- SERIAL / IDENTITY: Inteiro autoincremental gerenciado pelo banco. Simples e eficiente para índices B-tree.
- UUID: 128 bits, único globalmente. Bom para sistemas distribuídos, mas índices maiores e aleatórios.
- ULID: 26 caracteres, lexicalmente ordenável por timestamp e único. Menos aleatório que UUID e mais compacto.
Dica: pense em IDs como ferramentas operacionais. Escolha pelo comportamento que você precisa: previsibilidade, distribuição ou ordenação temporal.
Configuração do ambiente
Será necessário um ambiente mínimo: PostgreSQL (13+ recomendado), Node.js (16+), npm/yarn e Prisma. Abaixo comandos para Ubuntu/macOS (adaptáveis).
Instalar PostgreSQL e criar banco
Exemplo rápido para macOS com Homebrew; adapte para seu SO.
brew install postgresql
brew services start postgresql
createdb ids_demo
O que isso faz: instala o servidor Postgres, inicia o serviço e cria um banco chamado 'ids_demo'. Depois você pode conectar com psql ou uma GUI.
Node.js e Prisma
Crie um projeto Node, instale Prisma, o cliente e o pacote ULID se quiser gerar ULIDs no app.
mkdir ids-demo && cd ids-demo
npm init -y
npm install prisma @prisma/client ulid
npx prisma init
O comando npx prisma init cria um arquivo .env e uma pasta prisma/schema.prisma. Abra .env e ajuste DATABASE_URL para apontar ao seu 'ids_demo'. Exemplo:
DATABASE_URL='postgresql://postgres:password@localhost:5432/ids_demo?schema=public'
Depois disso você estará pronto para definir modelos e gerar migrações.
Hello World progressivo
Vamos implementar três tabelas em um mesmo banco para comparar comportamento: users_serial, users_uuid, users_ulid. Cada tabela terá schema, exemplo de inserção e leitura usando SQL puro e Prisma.
1) SERIAL / IDENTITY
O SERIAL é sintaticamente simples, mas modernamente recomendamos IDENTITY. Abaixo um CREATE TABLE usando IDENTITY (compatível com SQL padrão).
-- Cria tabela com id autoincremental (INTEGER) usando IDENTITY
CREATE TABLE users_serial (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT now()
);
O que acontece: Postgres cria uma sequência interna e atribui valores crescentes. Ótimo para índices compactos e joins rápidos. Desvantagem: coordenação se tiver múltiplas réplicas com escrita ou migrações que reescrevem IDs.
2) UUID
Usamos a função gen_random_uuid() do módulo pgcrypto. Se não tiver, instale com CREATE EXTENSION.
-- Habilita geracao de uuids aleatorios e cria tabela usando type uuid
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE users_uuid (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
email text NOT NULL UNIQUE,
created_at timestamptz DEFAULT now()
);
O que acontece: IDs 128-bit gerados no banco, únicos globalmente. Índices maiores que inteiros; inserts tendem a ficar espalhados (random) no índice, potencialmente causando page splits sob alta escrita.
3) ULID
Postgres não tem ULID nativo padrão amplamente disponível. Prática comum: gerar ULID no app e armazenar como char(26) ou varchar(26). Exemplo abaixo armazena ULID como texto fixo.
CREATE TABLE users_ulid (
id char(26) PRIMARY KEY,
email text NOT NULL UNIQUE,
created_at timestamptz DEFAULT now()
);
O que acontece: ULIDs são ordenáveis por tempo na representação textual, o que favorece inserções próximas no índice. Recomendado gerar no app com pacote 'ulid' para controle e previsibilidade.
Prisma: mapeamento e exemplos
Agora exponho modelos Prisma para cada abordagem. Em prisma/schema.prisma, mude datasource para apontar seu DATABASE_URL.
Modelo para SERIAL / autoincrement
Use Int com autoincrement. Assim o Prisma usa sequence/IDENTITY internamente no Postgres.
// prisma/schema.prisma (trecho)
model UserSerial {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now()) @map('created_at')
@@map('users_serial')
}
O que acontece: Prisma gera SQL para criar a tabela com integer autoincrement. As queries do client usam números e funcionam como em qualquer exemplo de ORM.
Modelo para UUID
Prisma tem helper @default(uuid()) para gerar no cliente; também é possível deixar o banco gerar com dbgenerated/ gen_random_uuid(). Ambas opções são válidas. Exemplo com @default(uuid()):
// prisma/schema.prisma (trecho)
model UserUuid {
id String @id @default(uuid()) @db.Uuid
email String @unique
createdAt DateTime @default(now()) @map('created_at')
@@map('users_uuid')
}
O que acontece: Prisma chama UUID no client antes do INSERT. Alternativamente, usar @default(dbgenerated('gen_random_uuid()')) delega ao banco. Escolha depende se você quer IDs antes do roundtrip.
Modelo para ULID
Prisma não tem @default(ulid()). Você gera ULID no app e insere. Definimos campo String com tamanho fixo.
// prisma/schema.prisma (trecho)
model UserUlid {
id String @id @db.Char(26)
email String @unique
createdAt DateTime @default(now()) @map('created_at')
@@map('users_ulid')
}
O que acontece: você precisa gerar id antes de criar o registro (no Node.js), por exemplo usando o pacote 'ulid'. Isso preserva ordenação temporal e gera chaves curtas.
Hello World com Prisma Client (exemplos)
A seguir, um script Node.js que insere um registro em cada tabela e consulta. Gere o client com 'npx prisma generate' e rode o script.
// scripts/demo.js
const { PrismaClient } = require('@prisma/client');
const { ulid } = require('ulid');
const prisma = new PrismaClient();
async function main() {
// SERIAL: id será autogerado no banco
const s = await prisma.userSerial.create({ data: { email: 'serial@example.com' } });
console.log('serial', s);
// UUID: prisma gera uuid() por @default(uuid()) no client
const u = await prisma.userUuid.create({ data: { email: 'uuid@example.com' } });
console.log('uuid', u);
// ULID: geramos aqui e passamos ao prisma
const ulidId = ulid();
const ul = await prisma.userUlid.create({ data: { id: ulidId, email: 'ulid@example.com' } });
console.log('ulid', ul);
}
main()
.catch(e => { console.error(e); process.exit(1); })
.finally(async () => { await prisma.$disconnect(); });
O que acontece: o script cria um registro em cada tabela. Observe os formatos de id nos logs: inteiro, uuid string e 26-char ulid.
Mini-projeto prático
Vamos montar um pequeno CRUD que demonstra paginação por primary key, vantagens de ULID para paginação baseada em cursor, e como lidar com joins e migration. Passos: criar schema Prisma, gerar migração, rodar script de seed e criar endpoints simples com Express (ou usar scripts CLI).
Migração e criação das tabelas
Use Prisma Migrate para criar tabelas conforme os modelos acima. Exemplo de comando:
npx prisma migrate dev --name init_ids_demo
O que acontece: Prisma cria arquivos SQL de migração e aplica ao banco. Verifique no psql que as tabelas users_serial, users_uuid e users_ulid existem e têm as colunas esperadas.
Seed básico
Crie um seed script para popular amostras de 100 registros em cada tabela, útil para testar performance e paginação. Exemplo simplificado:
// prisma/seed.js
const { PrismaClient } = require('@prisma/client');
const { ulid } = require('ulid');
const prisma = new PrismaClient();
async function seed() {
for (let i = 0; i < 100; i++) {
await prisma.userSerial.create({ data: { email: `s${i}@example.com` } });
await prisma.userUuid.create({ data: { email: `u${i}@example.com` } });
await prisma.userUlid.create({ data: { id: ulid(), email: `x${i}@example.com` } });
}
}
seed().finally(() => prisma.$disconnect());
O que acontece: popula as três tabelas. No caso do serial, os ids ficam 1..100; UUID serão randômicos; ULIDs ordenados por tempo de inserção.
Paginação por cursor: exemplo prático
Comparação rápida: paginar por id com autoincrement é trivial; com UUID você precisa de offset/limit ou cursor com coluna adicional created_at; ULID permite cursor baseado na string id por ser lexicograficamente ordenável.
// Exemplo de query para paginação baseada em ULID
SELECT * FROM users_ulid WHERE id > '01F8MECHZX3TBDSZ7XRADM79XV' ORDER BY id LIMIT 20;
-- Para UUID a mesma abordagem não funciona bem porque ordenacao por uuid nao representa tempo
-- Para UUID prefira created_at como cursor
O que acontece: ULID permite queries eficientes sem adicionar coluna created_at para cursor. UUID exige coluna extra se você quer ordenacao temporal.
Erros comuns, armadilhas e dicas práticas
Armadilha 1: escolher UUID por moda
Muitos escolhem UUID por acreditar que é sempre a melhor opção. Isso é mito. Se sua aplicação é monolítica e não precisa de IDs globais, um inteiro autoincrement é mais eficiente e simples de debugar.
Armadilha 2: esquecer o tamanho do índice
Index size importa. UUIDs aumentam o tamanho dos índices ~16 bytes por chave; ULIDs ~26 bytes em texto, ou 13 bytes se armazenados como binary (não padrão). Em tabelas muito grandes, o custo de I/O e cache aumenta. Meça antes.
Dicas práticas
- Se você precisa de unicidade global independentemente do banco: UUID é boa escolha.
- Se precisa de leitura de inserts ordenados por tempo e compactação: ULID oferece ordering temporal e chaves legíveis.
- Se quer simplicidade e índices menores: serial/identity é preferível.
- Gere IDs no cliente quando: você quer o ID antes do insert (ex: criar relacionamentos antes de persistir).
Dica: em sistemas distribuídos com escrita em múltiplas regiões, UUID ou ULID (gerados no app) evitam necessidade de coordenação para sequências.
Recursos para continuar estudando
Documentação e leitura adicional para se aprofundar e justificar escolhas em arquiteturas reais.
- Postgres SERIAL/IDENTITY – detalhes e diferenças entre SERIAL e IDENTITY.
- Postgres UUID – funções e extensões como pgcrypto e uuid-ossp.
- ULID spec – especificação formal e implementações.
- Prisma docs – como mapear campos e usar defaults.
Recomendo testes de carga com sua própria carga de trabalho: crie scripts que insiram milhões de registros e monitore índice, page splits e cache hit ratio.
Conclusão prática
Decisão técnica é contextual. Regra prática resumida:
- Se seu sistema é simples e centralizado: usE SERIAL/IDENTITY (Int autoincrement).
- Se precisa de unicidade global sem coordenação: usE UUID e avalie gerar no cliente ou no banco conforme necessidade.
- Se quer unicidade + ordenação temporal eficiente para paginação e um identificador curto: considere ULID, gerando no app e armazenando como char(26).
Com Prisma, o mapeamento é direto: autoincrement com Int, uuid() com String/@db.Uuid ou delegando ao banco, e ULID gerado no cliente salvo como String. Teste cada abordagem no seu cenário, meça latência e uso de disco, e priorize previsibilidade operacional nas decisões.
Agora você tem os passos para experimentar na prática: criar tabelas, gerar migrações com Prisma, popular dados e medir comportamento. Execute os exemplos acima, altere o volume e meça usando pg_stat_user_tables e explain analyze. Isso dará a resposta objetiva para o seu caso.
Assuntos: PostgreSQL, Prisma, uuid, ulid, serial, modelagem
Leia também
Indexando e consultando JSONB no PostgreSQL com GIN + pg_trgm e Prisma
Como indexar e consultar JSONB no PostgreSQL: combine GIN jsonb_path_ops para contenção com GIN+pg_trgm em expressões textuais, e integre via migrations SQL…
Usando INSERT ... ON CONFLICT (UPSERT) no PostgreSQL com exemplos em Prisma
Aprenda a usar INSERT ... ON CONFLICT (UPSERT) no PostgreSQL com exemplos práticos em Prisma. Entenda conceitos, armadilhas comuns e duas abordagens…
Interpretando EXPLAIN ANALYZE no PostgreSQL: guia prático para iniciantes
Guia prático para iniciantes sobre EXPLAIN ANALYZE no PostgreSQL: aprenda a rodar, interpretar planos, identificar problemas (seq scan, nested loop) e aplicar…
Migrações zero-downtime no PostgreSQL com Prisma: blue-green e backfill
Guia prático para executar migrações zero-downtime em PostgreSQL com Prisma. Cobertura de padrões expand-fill-contract, blue-green deploy, backfill em…
Transações no PostgreSQL com Prisma: commit, rollback e locks passo a passo
Guia prático e passo a passo de transacoes no Postgres com Prisma: como usar commit, rollback e locks, exemplos com transferencia entre contas, SELECT FOR…
Migrando um projeto Node + Prisma de SQLite para PostgreSQL: guia passo a passo para iniciantes
Guia prático para migrar um projeto Node + Prisma de SQLite para PostgreSQL. Aprenda conceitos, ajustes no schema, comandos de migração, importação de dados e…