DEV Community

Cover image for SetrixDB: motor de conjuntos em Go — interseção exata sobre IDs (e onde ele perde)
Thiago Silva
Thiago Silva

Posted on

SetrixDB: motor de conjuntos em Go — interseção exata sobre IDs (e onde ele perde)

“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)

  1. Keygen (MPHF): termo → ID uint64 determinístico e sem colisão.
  2. 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).
  3. Kernel: o AND dos 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.
  4. 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)