“Dado um ID, ele está nesta lista?” e “quais IDs aparecem nas duas listas ao mesmo tempo?” Parecem
exercícios de livro-texto. Mas quando essas listas têm milhões ou bilhões de elementos e precisam
responder em microssegundos — num filtro facetado, numa checagem de permissão, num pré-filtro de
candidatos para um LLM — a resposta deixa de ser trivial.
Este artigo é sobre uma primitiva específica: um motor de conjuntos exato sobre IDs uint64, com
números medidos e reproduzíveis — e uma seção dedicada a onde ele perde para uma biblioteca
consagrada. Não é sobre substituir bancos de dados; é sobre uma operação que costuma ficar em aberto.
O problema: álgebra de conjuntos sobre IDs
Boa parte do software moderno passa o tempo cruzando listas de identificadores:
- E-commerce / busca: “produtos desta cor E deste tamanho E desta marca E em estoque” — interseção de quatro conjuntos.
- Permissões / RAG: “quais documentos este usuário pode ver E casam com a busca?” — interseção de uma lista de permissão com uma lista de candidatos.
- Antifraude / acesso: “este ID está em alguma lista de bloqueio?” — pertencimento puro.
- Texto: cada termo ou frase vira uma chave; consultas combinadas são interseções.
Em todos esses casos o que importa é presença e interseção exatas sobre IDs — não payloads.
Estruturas genéricas (map, joins, varredura ordenada) resolvem isso — só não de forma otimizada:
carregam ponteiros, indireções e comparações desnecessárias quando o dado é o número.
A escolha central: trabalhar sempre com IDs uint64. Um conjunto é um monte de uint64; a
interseção é um AND. Tudo é aritmética.
O erro que valeu o projeto: 78% de colisão
Antes de existir conjunto, preciso de identificadores densos e sem colisão. O primeiro keygen foi um
hash posicional — uma fórmula aritmética simples. Ele colidia feio: numa base de 200 mil tokens
alfanuméricos curtos, 78% colidiam, e "Oa" e "0b" caíam no mesmo ID.
A correção foi implementar um Minimal Perfect Hash Function (CHD v2, do zero):
- 0 colisões numa base de 50 milhões de chaves;
- 4,03 bits/chave (≈ 24 MiB para 50M de chaves) — 3,4× menos memória que a v1 (13,68 bits/chave);
- lookup em ~118 ns, e o pertencimento continua exato.
Se este artigo tiver uma única lição, é esta: medir a taxa de colisão no corpus real é o passo
que quase todo mundo pula — e é o que muda a arquitetura inteira.
Como funciona (do termo ao resultado)
-
Keygen (MPHF): termo → ID
uint64determinístico e sem colisão. - Representação: o ID i vira o bit i de um bitset; há também conjunto esparso (lista ordenada) e um híbrido (faixas quentes em bitset + cauda fria esparsa).
-
Kernel: o
ANDdos bitsets roda em AVX-512 (vpandq+vpopcntq) via cgo, com dispatch em runtime (__builtin_cpu_supports) e fallback escalar — o mesmo binário funciona em qualquer máquina. -
Escala: o universo é fatiado em shards (paraleliza o compute); no cluster, cada nó
serve um shard, o coordenador faz broadcast e soma, e um consistent hash ring decide a posse
(entrar/sair um nó remapeia só ~
1/(N+1)dos IDs).
Os números reais
Ambiente (todas as medições): servidor de referência — 2 vCPU AMD EPYC (Zen4, AVX-512), 3,8 GB
RAM, Go 1.22 (+ gcc para o cgo). Data: 09/2026.
Pertencimento (n = 1M)
| Estrutura | memória | velocidade | exato? |
|---|---|---|---|
map[uint64] (Go) |
22,3 B/chave | 133,3M ops/s | sim |
| SetrixDB (MPHF CHD v2) | 0,5 B/chave (estrutura) | ~118 ns/lookup | sim |
| Bloom filter (1% falso-positivo) | 1,2 B/chave | 23,6M ops/s | não |
Paridade de velocidade com o map, com 2,2× menos memória — e resultado exato, ao contrário
de um filtro probabilístico.
Interseção (A = B = 1M)
| Estratégia | IDs densos (denso32) |
IDs aleatórios 64-bit (aleat64) |
|---|---|---|
| Merge ordenado (SetrixDB) | 9,2 ms | 11,3 ms |
| Roaring (bitmap comprimido) | 148 µs | 523 ms |
Hash join (map) |
91,6 ms | 94,9 ms |
Bitset AND (Go puro) |
29 µs | — |
Bitset AND (AVX-512) |
6 µs | — |
Onde ele perde (e por que isso importa)
Vou ser explícito, porque comparação sem contexto engana:
-
Universo esparso e enorme: quando o universo não cabe na RAM, o bitset denso deixa de ser
opção (ele ocupa
universo/8, sempre). Aí o Roaring ganha — é para isso que ele existe. Medido: universo 2²⁶, o Roaring64 usou ~2 MB contra 8,2 MB do meu bitset (mais lento em tempo, mais econômico em memória). -
IDs aleatórios de 64 bits: no caso
aleat64, o Roaring levou 523 ms — mas porque ele foi projetado para outro regime. O ponto não é “eu ganho sempre”; é em qual regime cada um brilha. - Range queries, similaridade, joins: o SetrixDB não faz. É igualdade pura.
- Atualizações frequentes: um MPHF é construído para um conjunto; inserir/remover chaves novas exige reconstrução. Para cargas mutáveis, ele não é a ferramenta.
Então onde ele ganha? No regime oposto: universo denso que cabe em RAM, conjuntos grandes,
interseção exata no caminho quente. Foi exatamente o que um teste com dados reais mostrou ↓
Dados reais (nada de só sintético)
Rodei três datasets públicos e conferi cada resultado por fora do SetrixDB (sort + comm).
Varejo — Online Retail II (UCI)
1.067.371 linhas reais de venda (UK, 2009–2011). Consulta “Reino Unido E 4º tri/2011 E
preço ≥ 5” → 22.701 linhas em 823 µs. Verificação independente: 22.701. Idêntico.
Texto — títulos da Wikipédia (19,3 milhões de termos)
enwiki-latest-all-titles-in-ns0: 19.264.252 títulos. “multi-palavra E começa com s” →
1.408.399 em 9,5 ms; “multi-palavra E United” → 38.602 em 8,0 ms. Verificado: idêntico.
Escala — MovieLens 25M (25 milhões de interações)
25.000.095 avaliações reais; facetas derivadas (gênero, década, nota). Conjuntos com 10,9M e
12,4M de membros. Três consultas, todas verificadas por fora:
| Consulta | Resultado |
|---|---|
| Drama E anos 2000 E nota ≥ 4 | 1.634.027 |
| Drama E nota ≥ 4 | 6.096.563 |
| Comédia E nota ≥ 4 E anos 2000 | 965.677 |
O resultado mais revelador deste teste é sobre escolher a representação certa. No mesmo
universo de 25M de IDs, com conjuntos de dezenas de milhões:
| Caminho | memória/conjunto | latência (A∩B) |
|---|---|---|
| Merge de listas ordenadas | 87,7 MB | 80,4 ms |
| Bitset denso (AVX-512) | 2 MB | 227 µs |
Mesmo resultado exato, ~350× mais rápido e ~43× menor. Quando o universo é denso e cabe na
memória, o bitset não é só o mais rápido: é o mais econômico também.
O que o SetrixDB É — e o que NÃO É
É um motor de conjuntos embarcável, em Go, que responde presença e interseção exatas
sobre IDs uint64, com kernel SIMD, sharding e modo de cluster. Ele coexiste com o seu banco
atual: o dado continua onde está; o SetrixDB entra ao lado como índice/pré-filtro.
Não é banco relacional, colunar, NoSQL ou vetorial. Não faz SQL, joins nem similaridade. E —
importante — guarda conjuntos de IDs, não payloads.
Limitações honestas
-
Alpha (
v0.1.0). Testado em loopback e entre duas máquinas; o cluster de 3 nós rodou em nuvem, mas ainda não em produção multi-datacenter. -
Memória do bitset é linear no universo (
universo/8); o híbrido mitiga (2³⁶ IDs: 8,59 GB densos → 1,73 MB híbridos), mas é uma escolha com custo. - Backend de NPU e protocolo UDP compacto: roadmap, não implementados.
- Benchmarks de energia (J/busca): planejados, ainda não medidos.
- Se algum número não se reproduzir na sua máquina, isso é um bug — e eu quero saber.
Perguntas difíceis (e respostas)
“Por que não usar CRoaring/Roaring direto?”
Porque o Roaring é excelente — e é a resposta certa quando o universo não cabe em RAM ou é muito
esparso. O SetrixDB mira outro ponto: Go nativo, embarcável, universo denso que cabe em RAM, com
MPHF no keygen e sharding/cluster integrados. Se o seu caso é o do Roaring, use o Roaring.
“Por que não um map/Bloom filter?”
map guarda ponteiros e é ~44× mais gordo por chave (22,3 vs 0,5 B/chave aqui). Bloom é menorzinho,
mas erra (1% de falso-positivo) — em permissão, errar para o lado “pode ver” é inaceitável.
“MPHF aguenta inserção/remoção?”
Não. Ele é construído para um conjunto. Cargas mutáveis exigem reconstrução (ou o modo esparso). É
uma troca consciente por lookup O(1) e ~4 bits/chave.
“E o GC do Go no caminho quente?”
Os bitsets são []uint64 contíguos, alocados uma vez; o laço quente não aloca. Para DMA (NPU) há
UnsafePtr + pinning — com o aviso de manter o buffer vivo.
“Isso não é só ‘bitset com AVX-512’?”
Em parte, sim — e é bom que seja: bitset + SIMD é uma base sólida e conhecida. O que o projeto
adiciona é o pacote: keygen sem colisão, representação adaptativa (denso/esparso/híbrido),
sharding/cluster e o modo “conjuntos armazenados” (só o nome trafega na rede).
O convite
O SetrixDB é open source (Apache-2.0). Se a próxima onda não é sobre guardar mais, mas sobre
decidir mais rápido — e se operações de conjunto merecem um motor dedicado, exato e vetorizado,
ao lado do que você já usa — venha testar.
Código, benchmarks reproduzíveis e quickstart: https://github.com/setrixdb/setrixdb
Rode os benchmarks, abra uma issue e me diga onde os números não fecham.
SetrixDB — the arithmetic set engine.
Conjuntos. Em microssegundos. Em qualquer chip. Ao lado do seu banco.
Licença: Apache-2.0 · Copyright 2026 SetrixDB.
Top comments (0)