42 pontos por GN⁺ 14 일 전 | 25 comentários | Compartilhar no WhatsApp
  • Todo banco de dados é, no fim das contas, um conjunto de arquivos estruturados sobre um sistema de arquivos, e aplicações em estágio inicial muitas vezes conseguem desempenho suficiente gerenciando arquivos diretamente
  • Implementando o mesmo servidor em Go, Bun e Rust e comparando três abordagens — varredura de arquivo, mapa em memória e busca binária em disco —, os resultados mostram que até o acesso simples a arquivos pode atingir alta taxa de processamento
  • A abordagem com mapa em memória teve o melhor desempenho (até 169 mil req/s), enquanto o SQLite ficou em 25 mil req/s, estável, mas com overhead
  • A maioria dos serviços consegue chegar a algo como 90 milhões de DAU usando apenas um único arquivo SQLite, e, na fase inicial do produto, um banco de dados separado pode ser desnecessário
  • A adoção de um banco de dados passa a ser necessária quando o dataset excede a RAM ou quando são necessários joins, buscas com múltiplas condições, escritas concorrentes e transações

Você realmente precisa de um banco de dados?

  • Bancos de dados são, no fim, conjuntos de arquivos; o SQLite é um único arquivo, enquanto o PostgreSQL é composto por diretórios e processos
    • Todo banco de dados lê e grava no sistema de arquivos, funcionando da mesma forma que chamar open() no código
    • Portanto, a questão central não é “vamos usar arquivos?”, mas sim “vamos usar os arquivos do banco de dados ou gerenciá-los diretamente?
    • Muitas aplicações em estágio inicial conseguem desempenho suficiente mesmo com gerenciamento direto

Configuração do experimento

  • O mesmo servidor HTTP foi implementado em Go, Bun (TypeScript) e Rust, comparando duas estratégias de armazenamento
    • Uso de três arquivos JSONL: users.jsonl, products.jsonl, orders.jsonl
    • Criação via POST /users e consulta via GET /users/:id
    • Apenas a rota de leitura (GET) foi usada no benchmark
  • Abordagem 1: ler o arquivo a cada requisição

    • A cada requisição, o arquivo é aberto, todas as linhas são percorridas, o JSON é parseado e o ID é comparado
    • Em média, é preciso ler metade do arquivo, então a complexidade é O(n)
    • Quanto maior o volume de dados, mais rapidamente o desempenho por requisição cai
  • Abordagem 2: carregar tudo na memória

    • Na inicialização, o arquivo inteiro é lido e armazenado em um hash map baseado em ID
    • As escritas são refletidas ao mesmo tempo no mapa e no arquivo, e as leituras viram uma consulta única ao mapa, com custo O(1)
    • O arquivo atua como armazenamento persistente, e o mapa como índice
    • Em Go, sync.RWMutex, e em Rust, RwLock, permitem leituras paralelas
  • Abordagem 3: busca binária no disco

    • Uma solução intermediária para consultas rápidas sem colocar todos os dados na RAM
    • Geração de um arquivo de dados ordenado por ID e de um arquivo de índice de largura fixa (58 bytes por registro)
    • O índice é pesquisado com ReadAt em O(log n) e então um único registro é lido no offset correspondente
    • Como novos registros quebram a ordenação, é preciso recriar periodicamente o índice ou fazer merge
    • Esse padrão de merge é semelhante ao funcionamento de uma LSM-tree

Ambiente de benchmark

  • Tamanho do dataset: 10 mil, 100 mil e 1 milhão de registros
  • Ferramenta de carga: wrk, executando requisições GET aleatórias por 10 segundos com 4 threads e 50 conexões concorrentes
  • Testes na mesma máquina (Apple M1 Mac mini, macOS 15) com Go 1.26, Bun 1.3 e Rust 1.94
  • Em Go, também houve comparação adicional com busca binária (disco) e SQLite (modernc.org/sqlite)

Principais resultados

  • Queda de desempenho na varredura linear: com 1 milhão de registros, Go caiu para 23 req/s e Bun para 19 req/s
  • Busca binária (disco): de 10 mil a 1 milhão de registros, caiu de 45 mil para 38 mil req/s, apenas 15%
    • Graças ao cache de páginas do SO, as partes superiores do índice permanecem sempre em memória
  • SQLite: manteve desempenho consistente, com 25 mil req/s e latência média de 2 ms
  • A busca binária foi cerca de 1,7x mais rápida que o SQLite, indicando overhead do SQLite em consultas simples por chave primária
  • O mapa em memória teve o melhor desempenho: de 97 mil a 169 mil req/s, com latência abaixo de 0,5 ms
  • Bun foi mais rápido que Go: Bun 106 mil req/s, Go 97 mil req/s
    • Bun é baseado em JavaScriptCore + Zig (uWebSockets), contornando o libuv
  • Rust dominou na varredura linear: de 3 a 6 vezes mais rápido que Go, provavelmente por eficiência em parsing de JSON e I/O
  • Melhor escolha por caso de uso

    • Maior throughput absoluto: Rust com mapa em memória (169 mil req/s)
    • Melhor sem depender de RAM para tudo: Go com busca binária (~40 mil req/s)
    • Quando SQL é necessário: SQLite (25 mil req/s)
    • Implementação mais simples: Go com varredura linear (~20 linhas de código)

O que significam 25.000 req/s

  • O tráfego web típico assume proporção pico:média = 2:1
    • Média de 12.500 req/s → pico de 25.000 req/s
  • Supondo usuários ativos fazendo 10 consultas por hora e taxa de simultaneidade de 10% no pico
    • Fórmula de requisições de pico: DAU × 0,000278
  • Resultado do cálculo de saturação em DAU para cada abordagem
    • Go com varredura linear: 2,8 milhões
    • Go com busca binária: 144 milhões
    • SQLite: 90 milhões
    • Go com mapa em memória: 349 milhões
    • Bun com mapa em memória: 381 milhões
    • Rust com mapa em memória: 608 milhões
  • A maioria dos produtos nunca chega a esses números
    • Ex.: um SaaS com 10 mil clientes → 3 req/s, um app com 100 mil DAU → 30 req/s
  • Em resumo, a maioria dos produtos iniciais não precisa de banco de dados
    • E, quando precisar, um único arquivo SQLite pode aguentar até 90 milhões de DAU

Quando um banco de dados passa a ser necessário

  • Quando o dataset já não cabe na RAM

    • Com dezenas de milhões de registros, só os índices já podem exigir vários GB
    • É necessário fazer paginação de dados, algo que o banco de dados resolve automaticamente
  • Quando é preciso consultar por campos além do ID

    • Buscas com múltiplas condições exigem varredura de arquivos ou mapas adicionais
    • Manter vários mapas equivale, na prática, a implementar manualmente um motor de consultas
  • Quando joins são necessários

    • É preciso ler e combinar múltiplos arquivos, e o SQL é mais eficiente para isso
  • Em caso de escritas concorrentes por múltiplos processos

    • Os mapas em memória de cada instância ficam separados, perdendo consistência
    • É necessário uma fonte externa única da verdade → papel de um banco de dados
  • Quando são necessárias escritas atômicas entre entidades

    • É preciso garantir sucesso/falha conjunta, como na criação de pedido e baixa de estoque
    • Isso exigiria implementar um log transacional separado, algo que o DB resolve com ACID
    • Ferramentas internas, side projects e produtos em fase inicial sem essas restrições
    • podem funcionar tranquilamente dentro da RAM de um único servidor
    • Arquivos JSONL também podem ser migrados facilmente para um banco de dados depois

Apêndice e código disponibilizado

  • Inclui código dos servidores em Go, Bun e Rust
  • Também há seed de dados e script separado para executar benchmarks (run_bench.sh)
  • O arquivo ZIP inclui go-server/, bun-server/, rust-server/ e seed.ts
  • O script gera dados nas três escalas, executa o teste de carga com wrk e encerra

Informações sobre o DB Pro

  • O DB Pro** é um cliente de banco de dados para Mac, Windows e Linux**

    • Reúne consulta, navegação e administração em uma só ferramenta
    • Oferece plataforma web colaborativa e IA embutida
    • Na versão mais recente, há suporte à conexão com bancos SQLite do Val Town
    • Na v1.3.0, foram adicionados criação de banco de dados, editor de múltiplas queries e conexão com PlanetScale Vitess

25 comentários

 
happing94 13 일 전

Que porra é essa? Você acha que se usa db por causa de performance?

 
myc0058 6 일 전

Bem típico de "armchair coding".

 
botplaysdice 13 일 전

Acho que é um texto muito bom. Especialmente materiais com esses 'números' são valiosos. Estamos numa época em que não é fácil encontrar desenvolvedores que tenham 'ao menos uma noção aproximada' de que tipo de overhead existe no código que criamos e na stack tecnológica que usamos, então li com prazer.

 
foriequal0 12 일 전

Também concordo. Acho que é um material que oferece uma intuição importante para mechanical sympathy e para calibrar o ritmo do desenvolvimento. Como "Latency Numbers Every Programmer Should Know", por exemplo.
E eu não li este texto como se uma direção específica fosse incondicionalmente melhor. Pelo contrário, os números mostrados por todas as abordagens mencionadas no texto me pareceram ser um "desempenho mais do que suficiente para a maioria dos negócios", então eu o li mais como uma proposta de escolher a abordagem adequada para cada situação-problema.

 
botplaysdice 13 일 전

As pérolas nas respostas são um bônus.

 
white9s 13 일 전

Se houver um motivo para fazer assim, aí talvez valha a pena considerar, certo? Algo como restrições de desempenho extremamente severas.
Mas, na maioria dos casos, existe mesmo algum motivo para escolher isso deliberadamente? Não é como se o DB não trouxesse vantagens...

 
m00nlygreat 13 일 전

Parece mais uma mudança de perspectiva, mas vocês todos estão bem sensíveis, hein.

 
tazuya 13 일 전

Pois é. Dá para encarar isso como uma sugestão de que, no início do negócio, quando ainda não há muitos usuários, em vez de comprar um banco de dados ou deixar tudo complexo, talvez seja possível levar a empresa até se firmar usando apenas I/O básico de arquivos.

 
smash8106 13 일 전

Eu também concordo. Às vezes, em serviços, o DB acaba sendo tratado como mais importante do que o necessário e, em alguns casos, até se faz um investimento excessivo em design, como se quebrar a normalização fosse causar um grande desastre.
Não se trata de dizer para não usar DB, mas sim de refrescar um pouco a visão sobre por que ele é usado e qual é, afinal, a essência do serviço.
No fim, equilíbrio é sempre importante.

 
cafedead 13 일 전

A partir do momento em que você escolhe o SQLite para um servidor de produção, precisa ficar pensando o tempo todo em quando vai migrar.
Antigamente, valia a pena pensar nisso porque o custo do próprio DB (compra de servidor, IDC, custo de licença etc.) era alto,
mas hoje em dia, quando dá para montar tudo com um simples clique, será que ainda há necessidade de se preocupar com isso?

 
roxie 8 일 전

Mesmo agora, banco de dados ainda é caro.

 
csjune 12 일 전

Claro, se for um “projeto em estágio inicial ou um aplicativo de pequena escala”, talvez nem precise de banco de dados. Não só banco de dados, como também dá para fazer os outros elementos de qualquer jeito. O problema é quando a escala cresce. No fim, é só um texto para ver números por diversão.

 
carnoxen 13 일 전

https://hackers.pub/@gnh1201/2025/…

Às vezes, não é necessário instalar um banco de dados separado. Embora seja limitado ao Windows...

 
roxie 8 일 전

Só de ver o título, já caí na risada

 
kuthia 13 일 전

Às vezes penso se as principais entidades realmente precisam ter sua persistência garantida por um RDBMS. Afinal, já existem várias tecnologias alternativas para fornecer uma SSOT.

 
neptune 13 일 전

Se o SQLite quebrar, não tem o que fazer..

 
okxrr 13 일 전

Há casos em que o sqlite quebra? Fiquei curioso. Excluindo movimentação ou exclusão anormal de arquivos.

 
GN⁺ 14 일 전
Comentários do Hacker News
  • Gostei muito deste texto. Ele mostra muito bem o quanto os computadores são rápidos
    Mas não concordo com a conclusão da parte final. O autor disse que isso não se aplica a muitos apps que têm a restrição de “vários processos precisarem escrever ao mesmo tempo”, mas na prática, mesmo em produtos em estágio inicial, muitas vezes há casos em que workers separados, como cron ou message queue, precisam escrever simultaneamente
    Dá para fazer com que só o servidor principal escreva, mas isso aumenta a complexidade da arquitetura
    Então, do ponto de vista puramente de escala, concordo com o autor, mas olhando de forma mais ampla, acho melhor usar um banco de dados. Especialmente o SQLite é uma escolha sensata
    Se precisar escalar, basta colocar em cache na memória os dados acessados com frequência. A combinação que eu uso é SQLite + cache em memória

    • Eu também passo por uma situação parecida com frequência. Mesmo que um único servidor seja suficiente, no momento em que passa a ser necessária redundância de servidores, você precisa de armazenamento em rede e acaba pendendo para um DB acessível pela rede
      Às vezes o S3 serve, mas ainda há muitas limitações para usá-lo como substituto completo
    • Hoje em dia, quando começo um projeto novo, uso SQLite por padrão. O desempenho é muito rápido e, se a escala crescer depois, é fácil migrar para Postgres
      É muito mais simples e barato porque não é preciso gerenciar nem fazer backup de um servidor de DB separado
    • Depois de ver o benchmark Rust 1M, percebi de novo o quanto os computadores são rápidos
  • Eu gosto muito de SQLite, mas percebi que ele não é a resposta para todos os problemas
    Ao criar um app cliente de dicionário, testei a porta wasm do SQLite, mas o arquivo do DB era maior do que eu esperava, não comprimía bem e também era lento para carregar
    No fim, mudei para uma abordagem em que crio índices diretamente a partir do arquivo TSV original, compacto com zstd e descompacto no wasm a cada vez. Isso ficou muito mais rápido do que SQLite
    O tamanho do módulo também caiu de 800KB para 52KB, e não havia peso mesmo ao subir várias instâncias ao mesmo tempo
    Para busca de strings usei stringzilla, e é absurdamente rápido
    SQLite é excelente, mas não é a resposta certa para toda situação

  • O benchmark de SQLite está pouco otimizado
    Só de adicionar

    db.SetMaxOpenConns(runtime.NumCPU())
    db.SetMaxIdleConns(runtime.NumCPU())
    

    assim, o desempenho na minha máquina saltou de 27.700 r/s para 89.687 r/s
    Tentei prepared statement e também trocar timestamp para int, mas não houve grande diferença

  • O texto estava ok, mas a parte de que “todo DB acessa o sistema de arquivos com open()” não é precisa
    Apps como o SQLite usam mmap para mapear diretamente o arquivo no espaço de memória. Dessa forma, é possível pular syscalls e acessar muito mais rápido
    Na parte posterior do texto, ele explica o processo de ler o arquivo inteiro para a memória, mas acho que teria sido melhor usar mmap

    • É verdade que o texto simplificou a explicação do IO do DB
      Mas também é difícil dizer que mmap é sempre melhor. Algumas pessoas preferem tratar isso diretamente na lógica da aplicação em vez de depender da API do SO
      Veja o artigo relacionado: estudo da CMU sobre mmap
    • O backend store usado por mmap no fim das contas também é um arquivo no sistema de arquivos
      A expressão “funciona como open()” é um pouco simplificada, mas tecnicamente está correta
  • Há muito tempo, fiz um pequeno webapp de vendas em Perl e, como eu não podia instalar nada no servidor do ISP, usei um hash baseado em arquivo
    O cliente usou aquilo por mais de 20 anos até falecer, e a família assumiu e trocou por Wordpress
    Na última vez que verifiquei, havia centenas de milhares de pedidos, e mesmo assim o desempenho estava bom
    Graças à evolução do hardware, essa gambiarra de arquitetura durou mais do que eu esperava. Hoje, acho que até SQLite teria sido suficiente

    • Fiquei curioso para saber que tipo de produto o site vendia
  • Se você implementar o armazenamento por conta própria, consegue entender como os DBs funcionam
    É preciso lidar com índices e estruturas de dados de forma eficiente e, no fim, a conclusão acaba sendo: “se não era brincadeira, você deveria ter usado um DB desde o começo”

  • Relational Databases Aren’t Dinosaurs, They’re Sharks
    Comparado ao pequeno ganho obtido em apps pequenos, o desperdício de tempo reinventando a roda é muito maior

    • A comparação entre tubarões e dinossauros é realmente apropriada
      Na era do Cretáceo, os tubarões já tinham quase a mesma forma de hoje e sobreviveram depois disso sem grandes mudanças
      Enquanto dinossauros, pterossauros e mosassauros desapareceram, tubarões, crocodilos e grandes cobras continuam quase iguais até hoje graças a um design otimizado
      Acho que os DBs relacionais são desse tipo
  • É prazeroso ler textos assim
    Mesmo assim, eu ainda uso DB com SQL e transações em 99% dos casos
    Mas recentemente, em um projeto pessoal, experimentei gerenciar dados com um sistema simples baseado em arquivos YAML, e na minha escala não houve nenhum problema de desempenho
    Ser legível por humanos e permitir diff era mais importante do que desempenho
    Ainda assim, na maioria dos casos eu escolheria um DB com linguagem de consulta e consistência garantida

  • No fim, você sempre acaba precisando dos recursos do DB e das garantias ACID
    Sempre que preciso usar um flat-file store legado, sofro tentando encaixar consistência, transações e linguagem de consulta à força. No fim, é só reinventar a roda

  • No momento em que a atomicidade se torna necessária, um DB é indispensável
    Implementar escrita atômica sobre um sistema de arquivos é muito frágil
    Por esse motivo, muitos DBs sofrem com problemas de corrupção de dados em caso de crash. Antigamente o RocksDB no Windows era assim

    • Se eu precisasse de alterações atômicas em arquivos, simplesmente usaria SQLite
      Implementar isso por conta própria parece loucura. Seria bom aprender a escrever com segurança usando a API do SO, mas hoje em dia isso é uma habilidade muito de nicho
      Além disso, há uma grande chance de a pessoa que vier depois não conseguir manter isso. No fim, acabaria migrando para um DB
    • O código do texto vai virar um arquivo vazio se um dia faltar energia
      No mínimo, é preciso escrever em um arquivo temporário no mesmo sistema de arquivos e, após fsync, substituir com rename
    • Em casos simples, não é tão frágil assim
      Se você escrever o DB inteiro em um arquivo temporário e depois substituir com flush e move, isso é atômico no Unix
      Só que isso não escala de jeito nenhum. Mesmo uma pequena atualização exige reescrever o arquivo inteiro, e também é preciso gerenciar locks. Isso resolve apenas parte do ACID
    • Vendo por esse lado, você já está lidando com o A do ACID
      Só para constar, o DB OLAP DuckDB também funciona muito bem em workloads out-of-core
    • Em 2025, Linux + ext4 dá suporte a escrita atômica de bloco único e de múltiplos blocos
      Link da documentação oficial
 
mstorm 13 일 전

Dá para viver sem geladeira, mas seria inconveniente.
Se você pode usar uma geladeira, não há motivo para não usar.

 
foobarman 12 일 전

Nora, você é apoiador do Ilbe?

 
alfenmage 5 일 전

Se eu disser não, então todo mundo é Ilbe? Eu sou da região de Gyeongsang, sabia?

 
foobarman 5 일 전

Quero denunciar, mas não sei como fazer uma denúncia. Aff.

 
okxrr 13 일 전

Parece que este comentário mostra o pensamento engessado dos desenvolvedores coreanos e o nível do GeekNews.

 
alfenmage 5 일 전

Que nível seria esse exatamente, por que você avaliou o nível dessa forma? Explique usando pelo menos dois entre lógica/fatos/ciência/estatística, beleza.

 
foobarman 5 일 전

Haha, só pelas palavras dá para ver que é um frequentador do DC Inside, Ilbe ou FM Korea, então não ligue para isso.