UUID vs ULID vs IDs seriais: escolhendo o ID certo para seu banco de dados
UUID vs ULID vs IDs seriais: entenda como cada um afeta indexação, ordenação, sharding e export/import de dados em projetos reais.

O problema real: escolher um ID de que você não se arrependerá depois
Escolher um ID parece chato na primeira semana. Depois você lança, os dados crescem e aquela decisão “simples” aparece em todo lugar: índices, URLs, logs, exports e integrações.
A verdadeira pergunta não é “qual é o melhor?” e sim “qual dor eu quero evitar mais tarde?”. IDs são difíceis de mudar porque eles acabam copiados em outras tabelas, em cache por clientes e dependidos por outros sistemas.
Quando o ID não combina com a evolução do produto, você normalmente percebe em alguns pontos:
- Inserções e consultas ficam mais lentas porque o índice da chave primária cresce de forma desfavorável.
- Paginação fica estranha quando ordenar por ID não corresponde ao tempo de criação.
- Migrações viram risco depois que outros sistemas dependem do formato do ID.
- Depuração leva mais tempo quando os IDs são difíceis de ler ou comparar.
- Imports colidem quando você mescla dados de várias fontes.
Sempre há um trade-off entre conveniência agora e flexibilidade depois. Inteiros seriais são fáceis de ler e costumam ser rápidos, mas podem vazar contagens de registros e dificultar a fusão de datasets. UUIDs aleatórios são ótimos para unicidade entre sistemas, mas prejudicam índices e são menos amigáveis para humanos. ULIDs tentam conciliar unicidade global com ordenação por tempo, mas ainda têm custos de armazenamento e ferramentas.
Uma forma útil de pensar: para quem é o ID principalmente?
Se o ID é mais para humanos (suporte, depuração, ops), algo curto e legível tende a vencer. Se é para máquinas (escritas distribuídas, clientes offline, multi-região), unicidade global e evitar colisões importam mais.
Definições rápidas em linguagem simples
Quando as pessoas debatem “UUID vs ULID vs IDs seriais”, elas estão escolhendo como cada linha recebe um rótulo único. Esse rótulo afeta o quão fácil é inserir, ordenar, mesclar e mover dados depois.
IDs seriais (integer/bigint)
Um ID serial é um contador. O banco distribui 1, depois 2, depois 3, e assim por diante (geralmente armazenado como integer ou bigint). É fácil de ler, barato de armazenar e normalmente rápido porque novas linhas vão para o fim do índice.
UUIDs
Um UUID é um identificador de 128 bits que parece aleatório, como 3f8a.... Na maioria dos cenários pode ser gerado sem pedir ao banco o próximo número, então sistemas diferentes podem criar IDs independentemente. A troca é que inserções com aparência aleatória podem exigir mais trabalho dos índices e ocupar mais espaço que um simples bigint.
ULIDs
Um ULID também tem 128 bits, mas é projetado para ser aproximadamente ordenado por tempo. ULIDs mais novos geralmente ordenam depois dos mais antigos, mantendo unicidade global. Você obtém parte do benefício de “gerar em qualquer lugar” dos UUIDs com um comportamento de ordenação mais amigável.
Resumo simples:
- Serial: menor e ordenado por padrão.
- UUID: o mais fácil de gerar independentemente, menos amigável ao humano, geralmente em ordem aleatória.
- ULID: independente como UUID, mas aproximadamente ordenado por tempo.
IDs seriais são comuns em apps com um único banco e ferramentas internas. UUIDs aparecem quando dados são criados entre múltiplos serviços, dispositivos ou regiões. ULIDs são populares quando equipes querem geração distribuída de IDs mas ainda se importam com ordenação, paginação ou consultas “mais recentes primeiro”.
Indexação e desempenho: o que muda na prática
Uma chave primária costuma ser apoiada por um índice (geralmente B-tree). Pense nesse índice como uma lista telefônica ordenada: cada nova linha precisa de uma entrada colocada no lugar certo para que buscas continuem rápidas.
Com IDs aleatórios (UUIDv4 clássico), novas entradas caem por toda a árvore do índice. Isso faz com que o banco toque muitas páginas de índice, divida páginas com mais frequência e faça escritas extras. Com o tempo você tem mais churn no índice: mais trabalho por inserção, mais misses de cache e índices maiores do que o esperado.
Com IDs em sua maioria crescentes (serial/bigint, ou IDs ordenados por tempo como muitos ULIDs), o banco geralmente consegue anexar novas entradas perto do fim do índice. Isso é melhor para cache porque as páginas recentes permanecem “quentes” e as inserções tendem a ser mais suaves sob altas taxas de escrita.
O tamanho da chave importa porque entradas de índice não são gratuitas:
- serial bigint: 8 bytes
- UUID: 16 bytes
- ULID: 16 bytes se armazenado como binário, muito maior se armazenado como string de 26 caracteres
Chaves maiores significam menos entradas por página de índice. Isso costuma levar a índices mais profundos, mais páginas lidas por consulta e mais RAM necessária para manter desempenho.
Se você tem uma tabela de “events” com inserções constantes, uma chave primária UUID aleatória pode começar a ficar mais lenta mais cedo que uma chave bigint, mesmo que buscas por linha única ainda pareçam normais. Se você espera escritas pesadas, o custo de indexação costuma ser a primeira diferença real que você nota.
Ordenação, paginação e ordem por tempo
Se você já construiu “Carregar mais” ou scroll infinito, já sentiu a dor de IDs que não ordenam bem. Um ID “ordena bem” quando ordenar por ele dá uma ordem estável e significativa (frequentemente o tempo de criação), então a paginação é previsível.
Com IDs aleatórios (como UUIDv4), linhas mais novas ficam espalhadas. Ordenar por id não corresponde ao tempo, e paginação por cursor como “me dê itens após este id” torna-se pouco confiável. Normalmente você recorre a created_at, o que é ok, mas precisa fazer com cuidado.
ULIDs foram projetados para serem aproximadamente ordenados por tempo. Se você ordenar por ULID (como string ou em forma binária), itens mais novos tendem a vir depois. Isso torna a paginação por cursor mais simples porque o cursor pode ser o último ULID visto.
O que ULID te dá (e o que não dá)
ULID ajuda com ordenação natural por tempo para feeds, cursores mais simples e menos inserções aleatórias que UUIDv4.
Mas ULID não garante ordem perfeita quando muitos IDs são gerados no mesmo milissegundo em máquinas diferentes. Se você precisa de ordenação exata, ainda quer um timestamp real.
Quando created_at ainda é melhor
Ordenar por created_at é frequentemente mais seguro quando você faz backfill, importa registros históricos ou precisa de desempate claro.
Um padrão prático é ordenar por (created_at, id), onde id é apenas o critério de desempate.
Sharding no futuro: evitando colisões de ID
Sharding significa dividir um banco em vários menores para que cada shard contenha parte dos dados. Times normalmente fazem isso mais tarde, quando um único banco fica difícil de escalar ou vira ponto único de falha.
Sua escolha de ID pode tornar o sharding gerenciável ou doloroso.
Com IDs sequenciais (auto-increment serial ou bigint), cada shard vai gerar 1, 2, 3.... O mesmo ID pode existir em vários shards. Na primeira vez que você precisar mesclar dados, mover linhas ou construir features cross-shard, aparecem colisões.
Você pode evitar colisões com coordenação (um serviço central de IDs ou ranges por shard), mas isso adiciona partes móveis e pode se tornar um gargalo.
UUIDs e ULIDs reduzem a coordenação porque cada shard pode gerar IDs independentemente com risco extremamente baixo de duplicatas. Se você acha que vai dividir dados entre bancos, esse é um dos argumentos mais fortes contra sequências puras.
Um plano simples que funciona (e o que custa)
Um compromisso comum é adicionar um prefixo de shard e então usar uma sequência local em cada shard. Dá para armazenar como duas colunas ou empacotar num único valor.
Funciona, mas cria um formato de ID customizado. Toda integração precisa entendê-lo, a ordenação deixa de significar ordem global por tempo sem lógica extra, e mover dados entre shards pode exigir reescrever IDs (o que quebra referências se esses IDs forem compartilhados).
Pergunte cedo: você alguma vez precisará combinar dados de vários bancos e manter referências estáveis? Se sim, planeje IDs globalmente únicos desde o início ou reserve orçamento para migrar depois.
Fluxos de exportação e importação de dados
Exportar e importar é onde a escolha de ID para de ser teórica. No momento em que você clona prod para staging, restaura um backup ou mescla dados de dois sistemas, você descobre se seus IDs são estáveis e portáveis.
Com IDs seriais (auto-increment), geralmente você não pode reaplicar inserts em outro banco esperando que referências permaneçam intactas, a menos que preserve os números originais. Se você importa apenas um subconjunto de linhas (por exemplo, 200 clientes e seus pedidos), precisa carregar tabelas na ordem certa e manter as mesmas chaves primárias. Se algo for renumerado, chaves estrangeiras quebram.
UUIDs e ULIDs são gerados fora da sequência do banco, então são mais fáceis de mover entre ambientes. Você pode copiar linhas, manter os IDs e os relacionamentos continuam batendo. Isso ajuda ao restaurar backups, fazer exports parciais ou mesclar datasets.
Exemplo: exportar 50 contas da produção para debugar um problema em staging. Com chaves primárias UUID/ULID, você pode importar essas contas mais linhas relacionadas (projetos, faturas, logs) e tudo ainda aponta para o mesmo pai. Com IDs seriais, muitas vezes você acaba criando uma tabela de tradução (old_id -> new_id) e reescrevendo chaves estrangeiras durante a importação.
Para imports em massa, o básico importa mais que o tipo de ID:
- Garanta que o importador não gere novos IDs por padrão.
- Importe pais antes dos filhos e valide chaves estrangeiras depois do carregamento.
- Se usar IDs seriais, resete as sequências ou o próximo insert pode colidir.
- Para ULIDs, armazene e exporte consistentemente (string vs binário).
Como escolher em 10 minutos
Você consegue tomar uma decisão sólida rapidamente se focar no que vai doer depois.
-
Anote seus maiores riscos futuros. Eventos concretos ajudam: dividir em múltiplos bancos, mesclar dados de outro sistema, escritas offline, cópias frequentes entre ambientes.
-
Decida se a ordenação por ID precisa casar com o tempo. Se você quer “mais recentes primeiro” sem colunas extras, ULID (ou outro ID ordenável por tempo) é uma opção limpa. Se ordenar por
created_atte serve, UUIDs e seriais funcionam. -
Estime volume de escrita e sensibilidade do índice. Se você espera inserções pesadas e o índice da chave primária será muito usado, um
BIGINTserial costuma ser menos custoso nos B-trees. UUIDs aleatórios tendem a causar mais churn. -
Escolha um padrão e documente exceções. Mantenha simples: um padrão para a maioria das tabelas e uma regra clara para quando desviar (frequentemente: IDs públicos vs IDs internos).
-
Deixe abertura para mudar. Evite codificar significado nos IDs, decida onde IDs são gerados (DB vs app) e mantenha constraints explícitas.
Erros comuns e armadilhas
A maior armadilha é escolher um ID porque está na moda e depois descobrir que ele não combina com como você consulta, escala ou compartilha dados. A maioria dos problemas aparece meses depois.
Falhas comuns:
- Usar UUIDs em todo lugar sem checar o custo. UUIDv4 pode inflar índices e reduzir a afinidade de cache. A aplicação ainda funciona, mas você paga com escritas mais lentas e backups maiores.
- Confiar em IDs seriais e depois precisar mesclar dados de múltiplos sistemas, regiões ou shards. Colisões aparecem em importações e syncs, e soluções rápidas como offsets e prefixos acabam vazando para todas as integrações.
- Assumir que ULID torna tudo mais rápido. Ajuda com ordem de inserção e paginação por tempo, mas não resolve joins lentos, índices faltantes ou linhas largas. Alguns geradores também não são estritamente monotônicos sob alta concorrência.
- Expor IDs sequenciais publicamente. Se suas URLs ou APIs usam
123, 124, 125, pessoas podem adivinhar registros vizinhos e sondar seu sistema. - Mudar o tipo de ID no meio do projeto sem plano de migração. Chaves estrangeiras, caches, logs e payloads externos podem continuar referenciando os antigos IDs muito tempo depois de você achar que mudou.
Sinais de alerta que você deve tratar cedo:
- Você espera imports de parceiros ou mesclas regulares entre ambientes.
- Você precisa de paginação ordenada por tempo sem depender de um timestamp separado.
- Você planeja compartilhar IDs fora do seu sistema (URLs, webhooks, apps móveis).
- Você não pode arcar com downtime para uma grande migração de IDs.
- Você espera tabelas muito grandes onde tamanho do índice e velocidade de escrita importam.
Checklist rápido antes de se comprometer
Realidade do banco e das consultas
Escolha um tipo de chave primária e mantenha-o na maioria das tabelas. Misturar tipos (bigint em um lugar, UUID em outro) complica joins, APIs e migrações.
Estime o tamanho do índice na escala esperada. Chaves mais largas significam índices primários maiores e mais IO e memória.
Decida como vai paginar. Se paginar por ID, garanta que o ID tenha ordenação previsível (ou aceite que não terá). Se paginar por timestamp, indexe created_at e use consistentemente.
Checagem para futuro
Teste seu plano de import em dados parecidos com produção. Verifique se você consegue recriar registros sem quebrar chaves estrangeiras e se reimports não geram IDs novos silenciosamente.
Escreva sua estratégia de colisões. Quem gera o ID (DB ou app) e o que acontece se dois sistemas criarem registros offline e depois sincronizarem?
Garanta que URLs públicas e logs não vazem padrões que você se importa (contagem de registros, ritmo de criação, dicas de shard). Se usar IDs seriais, presuma que as pessoas podem adivinhar IDs próximos.
Um exemplo realista: de MVP para dados multi-sistema
Um fundador solo lança um CRM simples: contatos, negócios, notas. Um Postgres, um web app, objetivo é entregar rápido.
No início, uma chave primária serial bigint parece perfeita. Inserções são rápidas, índices se mantêm organizados e é fácil ler nos logs.
Um ano depois, um cliente pede exports trimestrais para auditoria e o fundador começa a importar leads de uma ferramenta de marketing. IDs que eram internos agora aparecem em CSVs, e-mails e tickets de suporte. Se dois sistemas usam 1, 2, 3..., fusões viram bagunça. Você acaba adicionando colunas de origem, tabelas de mapeamento ou reescrevendo IDs durante importação.
No segundo ano, surge um app móvel que precisa criar registros offline e sincronizar depois. Agora você precisa de IDs que possam ser gerados no cliente sem falar com o banco e quer baixo risco de colisão quando os dados chegarem de ambientes diferentes.
Um compromisso que costuma envelhecer bem:
- Mantenha uma chave primária bigint para joins internos e eficiência de armazenamento.
- Adicione um ID público imutável separado (ULID, ou UUIDv7 se disponível) para compartilhamento, sincronização e imports.
- Use o ID público em arquivos de export e como chave de mesclagem entre sistemas.
Passos práticos para seu projeto
Se você está em dúvida entre UUID, ULID e seriais, decida com base em como seus dados vão se mover e crescer.
Escolhas rápidas para casos comuns:
- Ferramenta interna com um banco e baixo risco de integração: use
bigintserial como chave primária. - App público com URLs compartilháveis ou criação no cliente: use UUIDs (difíceis de adivinhar e seguras entre sistemas).
- SaaS que pode dividir por tenant ou região depois: use ULIDs (ou UUIDv7) para que novas linhas tendam a ficar próximas nos índices.
- Muitos imports de parceiros e dispositivos offline: evite IDs seriais puros para entidades externas.
Misturar frequentemente é a melhor resposta. Use bigint serial para tabelas internas que nunca saem do seu banco (tabelas de join, jobs em background) e UUID/ULID para entidades públicas como users, orgs, invoices e qualquer coisa que você possa exportar, sincronizar ou referenciar de outro serviço.
Se você está construindo no Koder.ai (koder.ai), vale decidir seu padrão de IDs antes de gerar muitas tabelas e APIs. O modo de planejamento da plataforma e snapshots/rollback facilitam aplicar e validar mudanças de esquema cedo, enquanto o sistema ainda é pequeno o bastante para mudar com segurança.
Perguntas frequentes
How do I choose between serial IDs, UUIDs, and ULIDs without overthinking it?
Comece com a dor futura que você quer evitar: inserções lentas por causa de índices aleatórios, paginação estranha, migrações arriscadas ou colisões de IDs em importações e fusões. Se você espera que os dados se movam entre sistemas ou sejam criados em vários lugares, prefira um ID globalmente único (UUID/ULID) e mantenha as preocupações sobre ordenação por tempo separadas.
When is a serial bigint primary key the best choice?
Serial bigint é uma ótima escolha quando você tem um único banco de dados, volumes de escrita elevados e os IDs permanecem internos. É compacto, rápido para índices B-tree e fácil de ler nos logs. A principal desvantagem é que é difícil mesclar dados depois sem colisões, e expor esses IDs pode revelar quantos registros você tem.
When should I use UUIDs as my primary key?
Escolha UUIDs quando registros podem ser criados em múltiplos serviços, regiões, dispositivos ou clientes offline e você quer risco extremamente baixo de colisão sem coordenação. UUIDs também funcionam bem como IDs públicos porque são difíceis de adivinhar. A contrapartida comum é índices maiores e inserções mais aleatórias em comparação com chaves sequenciais.
What’s the practical advantage of ULIDs over UUIDs?
ULIDs fazem sentido quando você quer IDs que possam ser gerados em qualquer lugar e que, em geral, ordenem por tempo de criação. Isso simplifica paginação por cursor e reduz a dor de inserções aleatórias que você vê com UUIDv4. Ainda assim, não trate ULID como um timestamp perfeito; use created_at quando precisar de ordenação estrita ou segurança em backfills.
Do random UUID primary keys really hurt performance in Postgres?
Sim, especialmente com UUIDv4 em tabelas com muitas escritas. Inserções aleatórias espalham as entradas pelo índice da chave primária, causando mais splits de página, churn de cache e índices maiores ao longo do tempo. Normalmente você percebe primeiro taxas de inserção sustentadas mais lentas e maior uso de memória/IO, não necessariamente consultas de linha única lentas.
Why does pagination get weird when I sort by UUID?
Ordenar por um ID aleatório (como UUIDv4) não vai corresponder à ordem de criação, então cursores do tipo “após este id” não produzem uma linha do tempo estável. A solução confiável é paginar por created_at e adicionar o ID como critério de desempate, por exemplo (created_at, id). Se você quer paginar apenas por ID, prefira um ID ordenável por tempo como ULID.
How does my ID choice affect sharding later?
IDs sequenciais colidem entre shards porque cada shard vai gerar 1, 2, 3... independentemente. Você pode evitar colisões com coordenação (faixas por shard ou um serviço central de IDs), mas isso adiciona complexidade operacional e pode se tornar um gargalo. UUIDs/ULIDs reduzem a necessidade de coordenação porque cada shard pode gerar IDs de forma segura por conta própria.
Which ID type is safest for exports, imports, and merging datasets?
UUIDs/ULIDs são mais fáceis porque você pode exportar linhas, importar em outro lugar e manter referências intactas sem renumerar. Com IDs seriais, importações parciais costumam exigir uma tabela de tradução (old_id -> new_id) e reescrita cuidadosa das chaves estrangeiras, o que é fácil de errar. Se você frequentemente clona ambientes ou mescla conjuntos de dados, IDs globalmente únicos economizam tempo.
Should I use one ID for everything, or have both internal and public IDs?
Um padrão comum é ter dois IDs: uma chave primária interna compacta (serial bigint) para joins e eficiência de armazenamento, e um ID público imutável (ULID ou UUID) para URLs, APIs, exportações e referências entre sistemas. Isso mantém o banco rápido e facilita integrações e migrações. O importante é tratar o ID público como estável e nunca reciclá-lo ou reinterpretá-lo.
What’s the safest way to commit to an ID strategy when building with Koder.ai?
Planeje isso cedo e aplique de forma consistente em tabelas e APIs. No Koder.ai, decida sua estratégia padrão de IDs no modo de planejamento antes de gerar muitas tabelas e endpoints, e use snapshots/rollback para validar mudanças enquanto o projeto ainda é pequeno. A parte mais difícil não é criar novos IDs — é atualizar chaves estrangeiras, caches, logs e integrações externas que continuam referenciando os antigos.