Ao desenvolver sistemas distribuídos e implementar diversos algoritmos de identificadores únicos — explorando desde UUIDs v0 a v8, CUID2, Nano ID e ULID no projeto UniqueIdsEncyclopediaEx — torna-se evidente que a escolha da chave primária dita o desempenho do banco de dados a longo prazo. O uso generalizado de UUIDs de 128 bits impõe um custo de espaço e sobrecarga de cache em motores como PostgreSQL e SQLite.
A adoção de Snowflake IDs de 64 bits (layout 41-10-12) elimina essa sobrecarga, garantindo ordenação cronológica natural, unicidade sem coordenação central e performance otimizada.
1. O Custo Oculto dos UUIDs na Árvore B-Tree
UUIDs de 128 bits (16 bytes) consomem o dobro de espaço de armazenamento por chave em comparação com inteiros de 64 bits (8 bytes). Em tabelas com dezenas de milhões de registros, essa duplicação esgota rapidamente o cache do buffer pool (shared_buffers no PostgreSQL ou cache_size no SQLite).
Além disso, UUIDs gerados aleatoriamente (v4) causam random page writes, fragmentando índices B-Tree e gerando page splits constantes no disco. Reduzir a chave primária para um inteiro de 64 bits compacta os nós internos da árvore, permitindo que mais índices caibam na memória RAM e acelerando drasticamente as consultas.
2. A Anatomia do Snowflake ID (Layout 41-10-12 com Epoch 2026)
O algoritmo Snowflake compacta três informações essenciais em um número inteiro de 64 bits (sendo 63 bits úteis para evitar estouro de sinal):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time (cont.) | Worker ID | Sequence |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-
Bit de Sinal (1 bit): Mantido em
0(garante compatibilidade comBIGINT). - Timestamp (41 bits): Milissegundos desde uma época customizada. Fixando a epoch em 1 de janeiro de 2026, garantimos aproximadamente 69 anos de operação contínua.
- Worker ID (10 bits): Permite até 1.024 nós concorrentes operando isoladamente sem coordenação central.
- Sequence (12 bits): Suporta a geração de até 4.096 IDs por milissegundo no mesmo nó.
3. Atribuição Determinística de Worker ID nos Desktops
Em arquiteturas Local-First offline, onde múltiplos clientes desktop rodam instâncias locais do banco de dados, atribuir um worker_id de forma centralizada pela rede é inviável.
A solução elegante na BEAM é derivar o worker_id de forma determinística a partir do nome do nó (Node.self()) utilizando a função de hash nativa :erlang.phash2/2:
# Para o nó de desktop local, ex: :"app_desktop_01@localhost"
worker_id = :erlang.phash2(Node.self(), 1024)
O phash2 distribui o hash uniformemente no intervalo de 0 a 1023 (espaço de 10 bits). Assim, cada estação de trabalho offline calcula seu próprio identificador de forma isolada no momento do boot, eliminando colisões de chaves primárias durante a sincronização futura.
4. O Perigo do MAX_SAFE_INTEGER no JavaScript
Um obstáculo comum ao adotar inteiros de 64 bits no backend é o tráfego de dados para o front-end. O JavaScript utiliza a especificação IEEE 754 de ponto flutuante de dupla precisão, o que limita a representação de inteiros seguros ao Number.MAX_SAFE_INTEGER ($2^{53} - 1$, ou 9.007.199.254.740.991).
IDs de 64 bits ultrapassam esse limite e, se trafegados como inteiros nativos no JSON, sofrerão arredondamentos irreversíveis pelo motor V8 do navegador. A solução no ecossistema Phoenix/Ecto é interceptar a serialização globalmente via protocolo Jason.Encoder, convertendo os inteiros gigantes para String:
defimpl Jason.Encoder, for: Integer do
def encode(value, opts) do
# Verifica se ultrapassa o limite seguro do JavaScript
if value > 9_007_199_254_740_991 or value < -9_007_199_254_740_991 do
Jason.Encode.string(Integer.to_string(value), opts)
else
Jason.Encode.integer(value, opts)
end
end
end
5. Integração com Schemas Ecto
Nos schemas do Ecto, abandonamos o :binary_id (comumente usado para UUIDs) em favor do tipo genérico :integer, que será mapeado automaticamente para colunas BIGINT no banco relacional.
defmodule MeuApp.Documento do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
@foreign_key_type :integer
schema "documentos" do
field :titulo, :string
timestamps()
end
def changeset(documento, attrs) do
documento
|> cast(attrs, [:titulo])
|> validate_required([:titulo])
|> maybe_assign_snowflake_id()
end
defp maybe_assign_snowflake_id(changeset) do
case get_change(changeset, :id) do
nil -> put_change(changeset, :id, MeuApp.Snowflake.generate_id())
_ -> changeset
end
end
end
Ao combinar essas técnicas, você reduz drasticamente o peso dos índices e o consumo de I/O nas suas instâncias de banco de dados, mantém a ordenação temporal natural das inserções, e garante uma operação resiliente em clientes desconectados que não dependem de coordenação externa para gerar chaves fortes.
Top comments (0)