Artigos aprofundados sobre a tecnologia que molda o que vem a seguir.

Bancos de Dados de Grafos Incorporáveis além do SQLite

Por que bancos de grafos estão ficando incorporáveis, o que o Rust traz para a mesa e quando um modelo de grafo supera o relacional para o seu caso de uso.

Pequeno cubo de nós conectados brilhantes aninhado dentro de uma carcaça mecânica enferrujada

O SQLite está em todo lugar. Está no seu celular, no navegador, na smart TV e, provavelmente, no seu carro. Ele resolveu um problema fundamental — dar às aplicações um banco de dados SQL completo sem precisar rodar um servidor separado — e fez isso tão bem que “banco de dados embutido” e “SQLite” viraram quase sinônimos. Mas o modelo relacional do SQLite nem sempre é a escolha certa. Se os seus dados são basicamente sobre relacionamentos — conexões sociais, grafos de dependência, redes de conhecimento, problemas de roteamento —, forçar tudo em tabelas com chaves estrangeiras gera consultas que são absurdamente complexas ou dolorosamente lentas.

Uma nova onda de bancos de dados de grafos incorporáveis está tentando fazer pelos dados em grafo o que o SQLite fez pelos dados relacionais: oferecer um banco rápido, sem dependências e que roda dentro do próprio processo, com a linguagem de consulta certa para dados conectados. Vários deles são escritos em Rust, o que acaba sendo um encaixe excelente para esse problema. Vamos ver por que os bancos de grafos estão ficando incorporáveis, o que o ecossistema Rust traz para a mesa e quando você realmente deveria considerar um deles.

O ponto cego do modelo relacional

Bancos relacionais lidam bem com a maioria dos padrões de dados. Um-para-muitos? Chave estrangeira. Muitos-para-muitos? Tabela de junção. Buscas simples, agregações, filtros — o SQL foi feito para isso. O problema começa quando suas consultas se importam com caminhos, profundidade e conectividade.

Pense em um resolvedor de dependências. Você tem pacotes, cada um dependendo de outros pacotes, e cada um com restrições de versão. Você precisa responder: “Se eu instalar o pacote X, qual é a árvore completa de dependências transitivas? Existe alguma dependência circular? Há requisitos de versão conflitantes em alguma profundidade?” Em SQL, isso exige CTEs recursivas (common table expressions), que são verbosas, difíceis de otimizar e ficam exponencialmente mais lentas conforme o grafo fica mais profundo.

-- Finding all transitive dependencies in SQL
-- This works, but gets ugly fast
WITH RECURSIVE deps AS (
-- Base case: direct dependencies
SELECT dependency_id, 1 as depth
FROM package_dependencies
WHERE package_id = 'my-package'
UNION ALL
-- Recursive case: dependencies of dependencies
SELECT pd.dependency_id, d.depth + 1
FROM package_dependencies pd
JOIN deps d ON pd.package_id = d.dependency_id
WHERE d.depth < 50  -- safety limit to prevent infinite loops
)
SELECT DISTINCT dependency_id, MIN(depth) as min_depth
FROM deps
GROUP BY dependency_id
ORDER BY min_depth;
-- Now try detecting circular dependencies.
-- Or finding the shortest path between two packages.
-- Or querying all packages within 3 hops that match a version constraint.
-- Each query gets progressively more painful.

Em um banco de grafos, esse é o padrão de consulta nativo. Você não está lutando contra o modelo de dados — está trabalhando com ele. Percorrer arestas, seguir caminhos, detectar ciclos: são operações de primeira classe, não recursão improvisada.

Por que ser incorporável importa

O Neo4j é o banco de grafos dominante há mais de uma década, e é genuinamente excelente. Mas ele é um servidor. Você o roda como um processo separado, se conecta por um protocolo de rede, gerencia a memória da JVM e lida com sua sobrecarga operacional. Para uma aplicação em produção com uma equipe de operações dedicada, tudo bem. Para uma ferramenta de linha de comando, um app desktop, um sistema de build ou um dispositivo embarcado, é absurdo.

A ideia do SQLite se aplica aqui: muitos casos de uso precisam de consultas em grafo sem a sobrecarga de um servidor. Uma ferramenta de análise de código que monta um grafo de chamadas. Um motor de jogo que armazena relacionamentos entre entidades. Um app de anotações local-first com links bidirecionais. Um analisador de topologia de rede. Todos eles querem consultas em grafo, e nenhum quer pedir que os usuários instalem e configurem um servidor de banco de dados.

Bancos de grafos incorporáveis rodam dentro do seu processo, guardam os dados em arquivos locais e expõem uma API de biblioteca em vez de um protocolo de rede. Sua aplicação faz link com eles da mesma forma que faria com o SQLite. Sem servidor, sem portas, sem autenticação, sem complexidade de deploy.

O que o Rust traz para os bancos de dados de grafos

O Rust virou a linguagem preferida de um número desproporcional de novos projetos de banco de dados, e os motivos vão além do discurso habitual de “segurança de memória sem coletor de lixo”.

  • Latência previsível. Travessias em grafos são sensíveis à latência. Cada salto em uma travessia é um acesso à memória, e travessias profundas fazem milhões deles. Uma pausa do GC no meio de uma travessia de 10 saltos estraga sua latência de cauda. O modelo de ownership do Rust oferece gerenciamento de memória determinístico, sem pausas de GC — essencial para uma performance de consulta consistente.
  • Concorrência segura. Bancos de grafos se beneficiam muito de travessias paralelas. Explorar vários caminhos simultaneamente pode transformar uma consulta de 100 ms em uma de 10 ms. O sistema de tipos do Rust previne data races em tempo de compilação, o que significa que você pode paralelizar agressivamente sem medo de corromper os dados do seu grafo.
  • Binário pequeno, sem runtime. Para um banco incorporável, o tamanho do deploy importa. Um banco de grafos em Rust compila para uma única biblioteca nativa, sem dependências de runtime. Compare isso com uma solução baseada na JVM que precisa de um runtime de 200 MB, ou uma solução em Go que embute um coletor de lixo que você não pediu.
  • FFI em C. A capacidade do Rust de expor uma API compatível com C significa que o banco pode ser usado em praticamente qualquer linguagem. Escreva em Rust, chame a partir de Python, JavaScript, Go, Swift ou qualquer outra linguagem que fale C.

O modelo de grafo de propriedades

A maioria dos bancos de grafos incorporáveis usa o modelo de grafo de propriedades, que vale a pena entender se você nunca trabalhou com bancos de grafos antes. O modelo tem três primitivas:

  • Nós — entidades com um rótulo e propriedades chave-valor. Pense neles como linhas de uma tabela, mas sem um esquema fixo.
  • Arestas — conexões direcionadas entre nós, também com rótulo e propriedades. O rótulo descreve o tipo de relacionamento (DEPENDS_ON, AUTHORED_BY, LINKS_TO).
  • Travessias — consultas que seguem arestas de nó em nó, opcionalmente filtrando por propriedades, agregando resultados ou encontrando caminhos.
// Conceptual API for an embeddable graph database
let db = GraphDB::open("my_graph.db")?;
// Create nodes
let alice = db.create_node("Person", props! {
"name" => "Alice",
"role" => "engineer"
})?;
let project = db.create_node("Project", props! {
"name" => "Auth Service",
"language" => "Rust"
})?;
// Create edges
db.create_edge(alice, project, "WORKS_ON", props! {
"since" => "2025-01"
})?;
// Traverse: find all projects worked on by Alice's teammates
let results = db.traverse()
.from(alice)
.follow("WORKS_ON")        // Alice -> projects
.reverse("WORKS_ON")       // projects <- other people
.follow("WORKS_ON")        // other people -> their projects
.filter(|node| node.label() == "Project")
.collect()?;

O modelo de grafo de propriedades é mais flexível que esquemas relacionais para dados que evoluem rapidamente. Você não precisa definir o esquema antecipadamente nem rodar migrações quando adiciona um novo tipo de relacionamento. Basta criar arestas com novos rótulos. Essa flexibilidade sem esquema é uma faca de dois gumes — você perde as garantias de segurança de um esquema bem definido —, mas, para aplicações em que a estrutura do grafo evolui (bases de conhecimento, redes sociais, rastreamento de dependências), é um trade-off pragmático.

Quando realmente usar um banco de grafos

Bancos de grafos costumam ser recomendados em situações em que são desnecessários, e ignorados em situações em que realmente ajudariam. Aqui está minha avaliação honesta de onde eles brilham e onde não brilham.

Bom encaixe: resolução de dependências, controle de acesso (quem pode acessar o quê por meio de quais grupos), detecção de fraudes (encontrar conexões entre entidades), motores de recomendação (usuários que curtiram X também curtiram Y), análise de topologia de rede, grafos de conhecimento e ferramentas de análise de código. O fio condutor: suas consultas naturalmente expressam caminhos e conectividade.

Mau encaixe: aplicações CRUD simples, dados de séries temporais, cargas de análise/agregação e qualquer coisa em que suas consultas sejam basicamente “buscar registros que atendam à condição X”. Se você está fazendo SELECT * FROM users WHERE country = 'US' ORDER BY created_at, um banco relacional é a ferramenta certa. Não use um banco de grafos só porque parece interessante — use porque suas consultas são genuinamente em formato de grafo.

O teste que eu uso: desenhe seu modelo de dados em um quadro branco. Se são basicamente caixas em linhas (entidades com atributos), use um banco relacional. Se são caixas conectadas por setas e as setas importam tanto quanto as caixas, considere um banco de grafos.

Motores de armazenamento e trade-offs

Por baixo do capô, bancos de grafos incorporáveis enfrentam decisões interessantes de motor de armazenamento. As abordagens mais comuns:

  • Armazenamento de lista de adjacência. Cada nó guarda uma lista de suas arestas de saída. Rápido para travessias locais (encontrar os vizinhos de um nó), mas lento para consultas globais (encontrar todas as arestas com rótulo X). A maioria dos bancos de grafos incorporáveis usa isso porque travessias locais são a operação mais comum.
  • Armazenamento de lista de arestas. As arestas ficam em uma estrutura ordenada separada, indexada por origem, destino ou rótulo. Melhor para consultas globais e operações em lote, mas adiciona indireção às travessias locais.
  • Abordagens híbridas. Alguns bancos usam listas de adjacência para travessias e mantêm índices secundários em rótulos de arestas ou propriedades de nós para consultas filtradas. É a opção mais flexível, mas usa mais armazenamento e torna as escritas mais caras.

Muitos bancos de grafos baseados em Rust são construídos sobre armazenamentos chave-valor incorporados já existentes, como RocksDB ou sled. É uma escolha pragmática: você ganha persistência testada em batalha, recuperação de falhas e compactação de graça. A camada de grafo mapeia nós e arestas para operações chave-valor. A desvantagem é que o teto de performance fica limitado pelas características do armazenamento chave-valor, e otimizações específicas para grafos (como guardar as arestas de um nó de forma contígua no disco para travessias amigáveis ao cache) podem ser mais difíceis de implementar.

Linguagens de consulta: a questão ainda em aberto

SQL é a linguagem universal dos bancos relacionais. Os bancos de grafos não têm um equivalente com consenso. Cypher (do Neo4j), Gremlin (do Apache TinkerPop), SPARQL (para grafos RDF) e o emergente padrão GQL disputam a atenção dos desenvolvedores.

A maioria dos bancos de grafos incorporáveis contorna isso oferecendo uma API com padrão builder na linguagem hospedeira, em vez de uma linguagem de consulta. Você constrói travessias com encadeamento de métodos, o que é ergonômico em linguagens tipadas e evita a complexidade de fazer parse e otimizar uma linguagem de consulta. O trade-off é que suas consultas não são portáveis entre bancos — trocar de um banco de grafos incorporável para outro significa reescrever o código das consultas.

GQL (Graph Query Language) é o padrão ISO que deve unificar as consultas em grafos. Ele toma emprestado muito do Cypher e vai ganhando adoção aos poucos. Se você está escolhendo um banco de grafos incorporável hoje, vale verificar se ele tem suporte a GQL no roadmap — linguagens de consulta padronizadas tendem a vencer no longo prazo, mesmo que as proprietárias sejam mais polidas no início.

Considerações práticas

Se você está avaliando um banco de grafos incorporável para o seu projeto, aqui está o que realmente importa na prática:

  1. Meça com a sua carga de trabalho. Benchmarks de bancos de grafos são notoriamente enganosos. Um banco que se sai bem em travessias rasas e largas (amigos de amigos em uma rede social) pode sofrer com travessias profundas e estreitas (resolução de cadeias de dependência). Faça protótipos com os seus padrões de consulta reais.
  2. Verifique a história de segurança contra falhas. Bancos incorporáveis vivem e morrem pelas suas garantias de durabilidade. O banco usa write-ahead logging? É seguro contra falhas? Consegue se recuperar de uma queda de energia sem perda de dados? “Corrupção em desligamento inesperado” não é uma resposta aceitável para produção.
  3. Olhe para o modelo de memória. Alguns bancos de grafos incorporáveis mapeiam o grafo inteiro em memória (memory-mapped), o que funciona muito bem até o grafo ultrapassar a RAM disponível. Outros usam uma abordagem disk-first com cache. Saiba qual modelo o banco escolhido usa e se o seu grafo cabe nele.
  4. Considere a qualidade dos bindings. Se o banco foi escrito em Rust, mas você o usa a partir de Python, os bindings de Python importam mais que os internos em Rust. Verifique se os bindings são mantidos, documentados e performáticos — ou se foram um mero complemento.
  5. Pense em migrações. Sem esquema não significa sem mudanças. Quando o seu modelo de grafo evoluir, como você vai lidar com os dados existentes? Alguns bancos suportam scripts de migração ou esquemas versionados. Outros deixam tudo por sua conta.

O espaço de bancos de grafos incorporáveis ainda é jovem comparado ao mundo relacional. O SQLite foi refinado por mais de duas décadas. A maioria dos bancos de grafos incorporáveis tem menos de cinco anos. Isso significa arestas mais ásperas, menos recursos da comunidade e mais risco. Mas a proposta de valor central — consultas em grafo sem sobrecarga de servidor — é sólida. Para o caso de uso certo, um banco de grafos incorporável pode substituir centenas de linhas de SQL recursivo por algumas linhas de código de travessia, rodar mais rápido nesse processo e fazer com que o seu modelo de dados realmente corresponda ao problema que você está resolvendo.