SSD Nodes Learn 8GB de RAM — $66/ano
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-02

DuckDB vs SQLite no servidor: use os dois

SQLite mantém o estado transacional; DuckDB consulta Parquet e CSV. Veja por que uma VPS costuma usar os dois, com exemplos práticos de cada um.

DuckDB vs SQLite em um servidor: a resposta em uma frase

SQLite é um mecanismo OLTP (processamento de transações online): armazena dados em linhas e foi projetado para ler e gravar poucas linhas por vez, com segurança e rapidez. DuckDB é um mecanismo OLAP (processamento analítico online): armazena dados em colunas e foi projetado para examinar milhões de linhas e retornar um agregado. Ambos são bibliotecas incorporadas, ambos abrem um arquivo comum e nenhum executa um processo de servidor que você precise supervisionar.

Portanto, a resposta honesta para "qual dos dois" quase sempre é "ambos, na mesma VPS". Sua aplicação mantém o estado atual no SQLite. Seus relatórios leem arquivos Parquet e CSV com o DuckDB. Eles não competem porque não executam a mesma função.

Por que o armazenamento por linhas e o armazenamento por colunas mudam a resposta

O SQLite grava uma linha como uma parte contígua de uma página. Buscar um pedido pela chave primária toca em uma página de índice e uma página de dados, o que corresponde a duas leituras. Isso é exatamente o que uma aplicação faz milhares de vezes por segundo: ler este usuário, atualizar esta sessão, inserir este pedido.

O DuckDB grava cada coluna separadamente e a compacta. Somar amount_cents em cinco milhões de linhas lê apenas a coluna amount_cents, ignora todos os outros bytes do arquivo e executa a soma usando código vetorizado em lotes de valores. As outras colunas nunca são lidas do disco, e é daí que vem o desempenho.

Agora execute cada mecanismo com a carga de trabalho do outro. Para somar uma coluna, o SQLite precisa percorrer cada linha e retirar a linha inteira da página para acessar um campo, portanto lê muito mais dados do disco do que precisa. Para inserir um pedido, o DuckDB precisa acessar o armazenamento de todas as colunas para gravar um único valor e obtém um bloqueio de escrita no arquivo inteiro do banco de dados. Nenhum dos mecanismos está com defeito. Cada um está respondendo a uma pergunta para a qual não foi projetado.

Onde o SQLite é adequado: estado transacional da aplicação

Escolha o SQLite quando as gravações forem pequenas, frequentes e não puderem ser perdidas. Sessões, pedidos, linhas de fila, configurações e qualquer outro dado criado por uma solicitação web.

sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/app

Crie a tabela e ative o write-ahead logging na mesma etapa.

sqlite3 /srv/app/app.db <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS orders (
  id           INTEGER PRIMARY KEY,
  customer     TEXT    NOT NULL,
  placed_at    TEXT    NOT NULL,
  amount_cents INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS orders_customer ON orders (customer);
INSERT INTO orders (customer, placed_at, amount_cents)
  VALUES ('ana', '2026-07-30T09:14:00Z', 4200);
SQL

A primeira linha da saída é wal. Esse é o PRAGMA que informa o modo para o qual o banco mudou, e é a configuração mais útil em um servidor. No modo padrão de rollback journal, uma operação de gravação bloqueia todos os leitores. No modo WAL, os leitores continuam lendo o último estado confirmado enquanto uma operação de gravação adiciona dados ao final. Assim, um relatório lento não atrasa a solicitação web que está aguardando.

Verifique se a linha foi retornada:

sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"

Você obtém 1|ana|2026-07-30T09:14:00Z|4200. Mais dois arquivos aparecem ao lado do banco de dados: app.db-wal e app.db-shm. Ambos pertencem ao banco de dados. Copiar apenas app.db enquanto a aplicação está em execução produz um backup inconsistente. Esse caso é explicado mais adiante.

O SQLite ainda permite apenas uma operação de gravação por vez. Esse limite é um bloqueio, não uma fila. Por isso, uma segunda operação de gravação que espere por tempo demais falha com database is locked em vez de bloquear indefinidamente. Aumente o tempo de espera usando PRAGMA busy_timeout = 5000; em todas as conexões abertas pela aplicação. Cinco segundos de espera eliminam a maioria desses erros em uma carga web normal.

Onde o DuckDB se destaca: análises sobre arquivos que você já tem

Escolha o DuckDB quando a pergunta começar com "quantos", "quanto" ou "quais são os dez primeiros", e a entrada for um conjunto de arquivos CSV ou Parquet. Instale o cliente de linha de comando, na versão 1.5.5 em julho de 2026:

curl https://install.duckdb.org | sh

O script instala o binário em ~/.duckdb/cli/latest/duckdb e exibe a linha que o adiciona ao seu PATH. Confirme se ele é executado:

~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"

Crie um arquivo realista para consultar. Este comando grava cinco milhões de linhas de pedidos em Parquet, compactadas com zstd:

mkdir -p /srv/data
duckdb :memory: "
COPY (
  SELECT i                                                     AS id,
         'cust_' || (i % 5000)                                 AS customer,
         TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (i) MINUTE AS placed_at,
         (i * 37) % 20000                                      AS amount_cents
  FROM range(5000000) t(i)
) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);"

Agora faça a consulta analítica. Abra o shell, ative o cronômetro e consulte o arquivo diretamente, sem uma etapa de importação:

.timer on
SELECT customer,
       count(*)                AS orders,
       sum(amount_cents)/100.0 AS revenue
FROM '/srv/data/orders.parquet'
GROUP BY customer
ORDER BY revenue DESC
LIMIT 5;

Leia seu próprio número em .timer em vez de confiar em um número publicado, porque o resultado depende do seu disco e da quantidade de núcleos. O formato do processo é o que importa. Não houve CREATE TABLE, nem INSERT, nem uma etapa de carregamento: o DuckDB leu o rodapé do Parquet, determinou quais grupos de colunas eram necessários para a consulta e leu somente esses grupos. Um diretório inteiro funciona da mesma forma com um glob, FROM '/srv/data/orders-*.parquet', transformando um mês de exportações diárias em uma única consulta.

A velocidade do disco é o limite inferior de tudo isso, e uma varredura de colunas é uma leitura sequencial longa. Por isso, a diferença entre armazenamento NVMe e SATA mais antigo em um VPS aparece aqui com mais clareza do que nas pequenas leituras aleatórias do SQLite.

Lendo seu banco de dados SQLite pelo DuckDB

Os dois mecanismos se integram pela extensão sqlite do DuckDB. Anexe o banco de dados da aplicação somente para leitura, para que uma consulta analítica nunca possa gravar no estado ativo:

INSTALL sqlite;
LOAD sqlite;
ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);
SELECT customer, count(*) AS orders
FROM app.orders
GROUP BY customer
ORDER BY orders DESC;

Isso lê as linhas do arquivo SQLite no momento da consulta, sem fazer uma cópia. É conveniente, mas não é rápido, porque os dados no disco ainda estão armazenados em linhas e o DuckDB precisa percorrê-los. Use isso para a exportação, não para um dashboard que recarrega a cada trinta segundos:

COPY (SELECT * FROM app.orders)
  TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);

Essa instrução é todo o padrão. O SQLite mantém as linhas ativas recentes. Uma exportação agendada transforma os períodos encerrados em Parquet. O DuckDB responde a todas as consultas que abrangem meses, e o banco de dados da aplicação permanece pequeno, o que mantém rápidas as gravações.

Execute a exportação em uma agenda, em vez de fazê-la manualmente. Um par de serviço e temporizador do systemd tem o tamanho adequado para isso: uma unidade que executa o COPY e um temporizador que a dispara todas as noites.

Executar ambos em um único VPS

Nada disso precisa de um contêiner, e nada precisa de uma porta. Ambos os mecanismos são bibliotecas, portanto a instalação consiste em um pacote e um caminho de arquivo. Se o restante da sua stack já estiver sendo executado em Docker Compose no mesmo VPS, monte o diretório de dados no contêiner que precisa dele em vez de adicionar um serviço de banco de dados, porque não há nenhum serviço para adicionar.

Duas regras evitam problemas nessa configuração.

Dê a cada mecanismo seu próprio diretório: /srv/app para o arquivo SQLite gravado pela aplicação e /srv/data para os arquivos Parquet lidos pelo sistema de analytics. Quando eles compartilham um diretório, uma tarefa de backup que cria um snapshot de um deles acaba concorrendo com o outro.

Não aponte dois processos para o mesmo arquivo de banco de dados DuckDB em modo de leitura e gravação. Apenas um processo pode manter um arquivo DuckDB aberto para gravação, e o segundo falha ao abri-lo. Vários leitores funcionam quando todos definem access_mode = 'READ_ONLY'. Isso surpreende quem vem do SQLite, no qual vários processos compartilham um arquivo rotineiramente. Se o seu sistema de analytics apenas lê arquivos Parquet, essa questão nunca surge, o que é mais um motivo para manter o estado persistente no SQLite.

Backups diferem, e a diferença causa problemas

Um banco de dados SQLite em execução é composto por três arquivos. Copiá-los com cp durante uma gravação gera um arquivo que abre, mas está incorreto. Use o próprio comando de backup do mecanismo. Ele cria um snapshot consistente enquanto o aplicativo continua gravando:

sqlite3 /srv/app/app.db ".backup '/srv/backup/app-$(date -u +%Y%m%dT%H%M%SZ).db'"
sqlite3 /srv/backup/app-20260730T091400Z.db "PRAGMA integrity_check;"

integrity_check imprime ok em uma cópia válida. Qualquer outro resultado significa descartar esse snapshot e criar outro.

Os arquivos Parquet nunca são alterados depois de gravados. Portanto, não precisam de tratamento especial: faça backup do diretório. Envie os dois caminhos para fora do servidor usando backups do restic a partir do seu VPS. Assim, toda a camada de dados é composta por dois diretórios em uma única tarefa de backup.

Modos de falha e as strings exatas que você verá

Error: database is locked do SQLite significa que outra conexão manteve o bloqueio de gravação por mais tempo do que o permitido pelo seu tempo limite. Isso não indica corrupção. Defina PRAGMA busy_timeout em todas as conexões e procure uma transação longa que deveria ter sido dividida em várias transações curtas.

Error: unable to open database file após uma alteração de permissões geralmente significa que o processo pode gravar no arquivo, mas não no diretório. O SQLite cria app.db-wal e app.db-shm ao lado do banco de dados. Portanto, o próprio diretório precisa ter permissão de gravação, e não apenas o arquivo .db.

IO Error: Could not set lock on file do DuckDB significa que um segundo processo já mantém o banco de dados aberto para gravação. Feche o outro shell ou abra o seu em modo somente leitura.

Out of Memory Error do DuckDB em uma VPS pequena significa que uma consulta precisou de mais memória de trabalho do que estava disponível. O DuckDB grava dados temporários no disco quando possível. Portanto, forneça um local para isso abrindo um arquivo de banco de dados no disco em vez de :memory: e limite o consumo com SET memory_limit = '2GB';. Em um servidor que executa outros serviços, esse limite impede que uma consulta ad hoc faça a aplicação ficar sem RAM.

Binder Error: Referenced column "amount" not found ao consultar Parquet quase sempre significa que o esquema do arquivo não é o que você lembrava. Execute DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; e leia os nomes reais das colunas.

Como escolher na prática

Pergunte qual é o padrão de gravação. Muitas gravações pequenas que precisam sobreviver a uma queda de energia indicam o SQLite. Pergunte qual é o padrão de leitura. Varreduras completas com agregações sobre um histórico longo indicam o DuckDB. A maioria dos sistemas reais responde sim às duas perguntas. Nesse caso, use cada mecanismo na parte em que ele é mais adequado, em vez de forçar um deles a substituir o outro.

A migração que deve ser evitada é mover o estado ativo da aplicação para o DuckDB porque um relatório estava lento. O relatório estava lento por causa do layout do armazenamento. Portanto, a correção é uma exportação, não uma reescrita do caminho de gravação.

FAQ

O DuckDB pode substituir o SQLite como banco de dados da minha aplicação?

Não para uma aplicação que grava com frequência. O DuckDB bloqueia a gravação do arquivo de banco de dados inteiro, permite um único processo de leitura e gravação por vez e é otimizado para alterações em massa, não para inserções de uma única linha. Mantenha o estado transacional no SQLite e deixe o DuckDB lê-lo com ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); quando um relatório precisar desses dados.

O DuckDB é realmente mais rápido que o SQLite para análises?

Para varreduras e agregações em uma tabela grande, sim. O motivo é a organização do armazenamento, não um ajuste de configuração. O DuckDB lê somente as colunas mencionadas pela consulta e processa os valores em lotes, enquanto o SQLite precisa percorrer linhas inteiras para acessar um campo. Para buscar uma única linha pela chave primária, a situação se inverte, porque o SQLite acessa duas páginas e o DuckDB acessa o armazenamento de todas as colunas.

Preciso de muita RAM para executar o DuckDB em uma VPS?

Não, mas defina um limite e disponibilize espaço em disco. Abra um arquivo de banco de dados em vez de :memory: para que o DuckDB possa gravar resultados intermediários no disco. Depois, defina SET memory_limit = '2GB'; com um valor que sua VPS possa disponibilizar. Sem um limite, um GROUP BY grande pode elevar Out of Memory Error ou expulsar outros serviços da RAM.

Como transfiro meus dados do SQLite para Parquet?

Anexe o arquivo SQLite pelo DuckDB e copie uma consulta diretamente com COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Execute esse procedimento em uma agenda para períodos encerrados, como as linhas do mês passado, e mantenha as linhas recentes no SQLite, onde a aplicação ainda grava dados.

Qual deles devo fazer backup e como?

Faça backup dos dois, de maneiras diferentes. Crie snapshots do SQLite com sqlite3 app.db ".backup '/srv/backup/app.db'" em vez de cp, porque um banco de dados em execução também é um -wal e um arquivo -shm, e uma cópia simples pode ficar inconsistente. Os arquivos Parquet nunca mudam depois de gravados, portanto basta copiar o diretório.

#duckdb#sqlite#database#analytics#parquet#self-hosting