Pular para o conteúdo
9 min de leitura

Chaves primárias: sequencial, UUID ou ULID?

Por Equipe Tech do Sonne ·

Auto-incremento, UUID e ULID comparados por unicidade, ordenação, tamanho, localidade de índice e segurança. Como escolher a chave certa sem arrependimento.

Neste artigo

Toda tabela precisa de uma forma de identificar cada linha de maneira única, e essa escolha — a chave primária — parece trivial até você percebê-la como uma das decisões mais difíceis de reverter num sistema. A chave primária se espalha por todas as chaves estrangeiras que a referenciam, aparece em URLs, viaja em APIs, entra em índices e caches. Trocá-la depois que o sistema cresceu é uma cirurgia de grande porte. Por isso vale a pena entender as opções antes de gravar a decisão em pedra.

As três abordagens dominantes são o inteiro sequencial (auto-incremento), o UUID e o ULID. Cada uma otimiza para coisas diferentes e cobra o preço em outras. O sequencial é compacto e ordenado, mas centralizado e previsível. O UUID é único em qualquer lugar sem coordenação, mas grande e desordenado. O ULID tenta ficar com o melhor dos dois: único como o UUID, mas ordenável no tempo como o sequencial. Não existe escolha universalmente certa; existe a escolha certa para as suas restrições.

Vamos analisar cada opção pelos critérios que realmente importam: unicidade e geração, tamanho e desempenho de índice, ordenação, e segurança. Ao final, um método de decisão que evita os arrependimentos mais comuns.

O inteiro sequencial: compacto e ordenado#

A chave sequencial é um inteiro que o banco incrementa automaticamente a cada inserção: 1, 2, 3, e assim por diante. É a opção tradicional, implementada por SERIAL, IDENTITY ou AUTO_INCREMENT conforme o banco.

Suas virtudes são reais. É compacto: um inteiro de oito bytes ocupa pouco espaço, o que importa muito porque a chave primária é replicada em cada índice e em cada chave estrangeira que a referencia. É naturalmente ordenado: as chaves crescem monotonicamente, o que produz uma propriedade valiosa chamada localidade de índice. Como as inserções sempre acrescentam valores maiores que os anteriores, elas caem sempre na "ponta" da estrutura de índice, sem espalhar escritas por toda ela. Isso mantém o índice compacto e as inserções rápidas. E é legível por humanos: um id como 42 é fácil de comunicar, comparar e depurar.

Os problemas do sequencial aparecem em dois eixos. O primeiro é a coordenação. Gerar o próximo número exige um ponto central que garanta que dois inserts simultâneos não recebam o mesmo id. Num banco único isso é trivial, mas quando você distribui a escrita por várias máquinas, ou quer gerar o id no cliente antes de tocar o banco, o contador centralizado vira um gargalo e um ponto de acoplamento. O segundo é a previsibilidade, que é um problema de segurança e de negócio. Se os ids são sequenciais e aparecem em URLs, qualquer um percebe que o pedido 1001 vem depois do 1000, pode adivinhar ids válidos para tentar acessar (o clássico ataque de referência direta insegura, o IDOR) e ainda inferir informação de negócio — quantos usuários você tem, a que ritmo cresce — só observando os números. Expor contadores sequenciais vaza dados sem querer.

O UUID: único em qualquer lugar, sem coordenação#

O UUID é um identificador de 128 bits projetado para ser único globalmente sem nenhuma coordenação central. Você pode gerá-lo em qualquer máquina, no cliente ou no servidor, com probabilidade desprezível de colisão, e ele será único mesmo entre sistemas que nunca se falaram.

Essa é a grande virtude: geração descentralizada. Você cria o id onde quiser, antes mesmo de inserir no banco, o que simplifica enormemente sistemas distribuídos, sincronização offline, e a montagem de grafos de objetos relacionados antes de persistir. Não há contador central, não há gargalo, não há ida ao banco só para descobrir o próximo id. E o UUID (nas versões aleatórias) é imprevisível: dado um UUID, você não consegue adivinhar outro válido nem inferir volume ou ordem de criação. Isso fecha a porta para adivinhação de ids e para vazamento de métricas de negócio.

O preço tem duas faces. A primeira é o tamanho: 128 bits são o dobro de um inteiro de oito bytes, e essa diferença se multiplica por cada índice e cada chave estrangeira, inchando o armazenamento e reduzindo quantas entradas cabem em cada página de índice — o que pressiona o cache e a memória. A segunda, e mais séria em bancos tradicionais, é a desordem. UUIDs aleatórios não têm relação com o tempo de criação; cada novo id cai numa posição aleatória do índice. Isso destrói a localidade que o sequencial tinha de graça: as inserções se espalham por toda a estrutura, causando fragmentação e forçando o banco a mexer em muitas páginas diferentes a cada escrita. Em tabelas grandes com muita inserção, essa aleatoriedade degrada visivelmente o desempenho de escrita e engorda o índice.

Nem todo UUID é aleatório#

Vale a nuance de que "UUID" não é uma coisa só. Existem versões diferentes. As versões puramente aleatórias entregam a imprevisibilidade máxima mas sofrem toda a desordem. Existem também versões e variantes mais novas desenhadas para embutir um componente temporal no início do valor, justamente para recuperar a ordenação e a localidade de índice sem abrir mão da geração descentralizada. Essa evolução aproxima o UUID da ideia que o ULID popularizou.

O ULID: o meio-termo ordenável#

O ULID é um identificador de 128 bits, como o UUID, mas construído para ser ordenável no tempo: seus primeiros bits codificam um carimbo de tempo, e o restante é aleatório. A intenção é combinar a unicidade descentralizada do UUID com a localidade de índice do sequencial.

Como o prefixo é temporal, dois ULIDs gerados em momentos diferentes ordenam-se naturalmente pela ordem de criação, e inserções sucessivas caem em posições próximas do índice, recuperando boa parte da localidade que o UUID aleatório perdia. Ao mesmo tempo, a parte aleatória garante unicidade mesmo para vários ids gerados no mesmo instante e em máquinas diferentes, sem coordenação. O ULID ainda costuma ser representado numa codificação textual mais curta e amigável que a representação canônica do UUID, o que facilita o uso em URLs e logs.

O ULID herda a imprevisibilidade parcial: como o prefixo é temporal, um observador consegue inferir quando um registro foi criado a partir do id, e a ordem relativa de criação fica exposta. Isso normalmente é um vazamento menor que o dos ids sequenciais — você não adivinha outros ids válidos a partir de um ULID, porque a parte aleatória é imprevisível — mas para casos em que até o instante de criação é sensível, é uma consideração a pesar.

O ponto de atenção mais prático com ULID é operacional, não conceitual. Ele é frequentemente usado sobre colunas do tipo texto ou binário, e converter entre a representação textual do ULID e o tipo nativo de identificador do banco é fonte recorrente de bugs sutis. Guardar a representação de um ULID numa coluna que espera outro formato, ou misturar as codificações entre camadas, produz erros que só aparecem em tempo de execução. A decisão de usar ULID vem acompanhada da disciplina de padronizar a conversão numa borda única do sistema, e de garantir que a codificação usada seja consistente em todo lugar.

Comparando pelos critérios que importam#

Reunindo os eixos de decisão em prosa, o quadro fica claro. Em compactação, o sequencial vence com folga, e UUID e ULID empatam no dobro do tamanho. Em geração descentralizada, UUID e ULID vencem, e o sequencial perde por depender de coordenação central. Em localidade de índice e desempenho de escrita, o sequencial e o ULID vão bem, e o UUID aleatório vai mal. Em imprevisibilidade e segurança, o UUID aleatório vence, o ULID fica no meio (esconde o id mas revela o tempo), e o sequencial perde feio ao expor ordem e volume. Em legibilidade humana, o sequencial é o mais amigável, seguido do ULID pela codificação mais curta, com o UUID sendo o mais verboso.

Nenhuma opção domina todas as colunas. É por isso que a escolha depende de qual eixo é decisivo para o seu sistema.

Um método de decisão#

Comece perguntando se você precisa gerar ids fora do banco ou de forma distribuída. Se sim, o sequencial está fora, e a disputa é entre UUID e ULID. Se não — você tem um banco central que sempre gera o id — o sequencial volta a ser candidato forte pela compactação e simplicidade.

Depois, pergunte se o id vai aparecer publicamente, em URLs ou APIs. Se sim, o sequencial é perigoso pela previsibilidade, e você quer UUID ou ULID pela imprevisibilidade. Um padrão comum e sensato é usar sequencial como chave interna e um UUID/ULID como identificador público, tendo o melhor dos dois — mas isso adiciona uma coluna e uma indireção que precisa ser justificada.

Por fim, pese o desempenho de escrita. Se a tabela recebe muitíssimas inserções e o desempenho de índice importa, evite o UUID puramente aleatório; prefira o ULID ou uma variante temporal de UUID, que preservam a localidade. Para tabelas de volume moderado, a desordem do UUID aleatório costuma ser irrelevante e a simplicidade compensa.

O erro que se paga caro#

O arrependimento mais comum não é escolher a opção "errada" em abstrato, e sim escolher sem pensar nos eixos e descobrir tarde. Adotar sequencial e depois precisar distribuir a escrita. Adotar UUID aleatório e ver o índice inchar numa tabela de bilhões de linhas. Adotar ULID e sofrer com conversões de tipo espalhadas pelo código. Como a chave primária é cara de trocar, o tempo gasto pesando esses critérios no início é o investimento com melhor retorno da modelagem.

Fechando o raciocínio#

Escolher a chave primária é escolher qual trade-off você prefere carregar pelo resto da vida do sistema. O sequencial oferece compactação, ordenação e legibilidade, ao custo de coordenação central e previsibilidade perigosa. O UUID oferece geração descentralizada e imprevisibilidade, ao custo de tamanho e — na variante aleatória — desordem de índice. O ULID busca o meio-termo, unindo unicidade descentralizada e localidade temporal, ao custo de expor o instante de criação e de exigir disciplina na conversão de tipos.

A decisão certa cai de maneira natural quando você é honesto sobre suas restrições reais: onde os ids são gerados, se aparecem em público, e quanto o desempenho de escrita importa. Responder a essas três perguntas com sinceridade vale mais do que qualquer preferência estética por números bonitos ou identificadores globalmente únicos.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly