DuckDB vs SQLite no servidor: use os dois
SQLite mantém o estado transacional; DuckDB analisa Parquet e CSV. Veja por que uma VPS costuma usar ambos e confira um exemplo prático de cada.
DuckDB vs SQLite num servidor: a resposta numa frase
SQLite é um motor OLTP (processamento de transações online): armazena os dados em linhas e foi concebido para ler e escrever poucas linhas de cada vez, com segurança e rapidez. DuckDB é um motor OLAP (processamento analítico online): armazena os dados em colunas e foi concebido para analisar milhões de linhas e devolver um único resultado agregado. Ambos são bibliotecas incorporadas, ambos abrem um ficheiro simples e nenhum executa um processo de servidor que tenha de administrar continuamente.
Por isso, a resposta honesta à pergunta "qual deles" é quase sempre "ambos, na mesma VPS". A sua aplicação mantém o estado operacional no SQLite. Os relatórios leem ficheiros Parquet e CSV com DuckDB. Não competem porque não executam a mesma função.
Por que o armazenamento orientado a linhas e o armazenamento orientado a colunas mudam a resposta
O SQLite grava uma linha como uma peça contígua de uma página. Buscar um pedido pela chave primária toca numa página de índice e numa página de dados, o que corresponde a duas leituras. É exatamente o que uma aplicação faz milhares de vezes por segundo: ler este utilizador, atualizar esta sessão, inserir este pedido.
O DuckDB grava cada coluna separadamente e comprime-a. Somar amount_cents em cinco milhões de linhas lê apenas a coluna amount_cents, ignora todos os outros bytes do ficheiro e executa a soma com código vetorizado sobre lotes de valores. As outras colunas nunca são lidas do disco. É daí que vem a velocidade.
Agora execute cada engine com a carga de trabalho da outra. Para somar uma coluna, o SQLite tem de percorrer todas as linhas e retirar a linha inteira da página para chegar a um campo. Assim, lê muito mais dados do disco do que precisa. Para inserir um pedido, o DuckDB tem de tocar no armazenamento de todas as colunas para gravar um único valor e obtém um bloqueio de escrita sobre todo o ficheiro da base de dados. Nenhuma das engines está avariada. Cada uma está a responder a uma pergunta para a qual não foi concebida.
Onde o SQLite se destaca: estado transacional da aplicação
Escolha SQLite quando as gravações forem pequenas, frequentes e não puderem ser perdidas. Sessões, pedidos, linhas de filas, definições e tudo o que um pedido Web criar.
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appCrie a tabela e ative o write-ahead logging no mesmo passo.
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);
SQLA primeira linha da saída é wal. Esse é o PRAGMA que indica o modo para o qual mudou, e é a definição mais útil num servidor. No modo predefinido de journal de rollback, uma gravação bloqueia todos os leitores. No modo WAL, os leitores continuam a ler o último estado confirmado enquanto uma gravação é anexada. Assim, um relatório lento já não atrasa o pedido Web que está à espera.
Confirme que a linha foi devolvida:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"O resultado é 1|ana|2026-07-30T09:14:00Z|4200. Apareceram mais dois ficheiros junto à base de dados, app.db-wal e app.db-shm, e ambos pertencem a ela. Copiar apenas app.db enquanto a aplicação está em execução produz uma cópia de segurança inconsistente. Isto é explicado mais abaixo.
O SQLite continua a permitir apenas uma gravação de cada vez. Esse limite é um bloqueio, não uma fila. Por isso, uma segunda gravação que espere demasiado tempo falha com database is locked em vez de bloquear indefinidamente. Aumente o tempo de espera com PRAGMA busy_timeout = 5000; em todas as ligações que a aplicação abrir. Cinco segundos de espera eliminam a maioria destes erros numa carga Web normal.
Onde o DuckDB se destaca: análise de ficheiros que já tem
Escolha o DuckDB quando a pergunta começa por "quantos", "quanto" ou "quais são os dez primeiros", e a entrada é um conjunto de ficheiros CSV ou Parquet. Instale o cliente de linha de comandos, versão 2.0.0 (uma versão LTS) em 27 de outubro de 2026:
curl https://install.duckdb.org | shO script instala a versão estável mais recente, exceto se fixar uma versão, por exemplo curl https://install.duckdb.org | DUCKDB_VERSION=1.5.6 sh para permanecer na versão anterior. Instala o binário em ~/.duckdb/cli/latest/duckdb e apresenta a linha que o adiciona ao seu PATH. Confirme que é executado:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Crie um ficheiro realista para consultar. Isto grava cinco milhões de linhas de encomendas em Parquet, comprimidas 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);"Faça agora a pergunta analítica. Abra a shell, ative o temporizador e consulte o ficheiro 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 o seu próprio número em .timer em vez de confiar num valor publicado, porque o resultado depende do seu disco e do número de cores. O formato do resultado é o que importa. Não houve CREATE TABLE, INSERT nem uma etapa de carregamento: o DuckDB leu o footer do Parquet, determinou de que blocos de colunas a consulta precisava e leu apenas esses blocos. Um diretório inteiro funciona da mesma forma com um glob, FROM '/srv/data/orders-*.parquet', permitindo transformar um mês de exportações diárias numa única consulta.
A velocidade do disco é o limite inferior de tudo isto, e uma leitura de colunas é uma leitura sequencial longa. Por isso, a diferença entre armazenamento NVMe e SATA mais antigo numa VPS é mais evidente aqui do que nas pequenas leituras aleatórias do SQLite.
Ler a sua base de dados SQLite a partir do DuckDB
Os dois motores comunicam através da extensão sqlite do DuckDB. Anexe a base de dados da aplicação em modo somente leitura, para que uma consulta analítica nunca possa escrever 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;Isto lê as linhas do ficheiro SQLite no momento da consulta, sem criar uma cópia. É conveniente, mas não é rápido, porque os dados no disco continuam armazenados por linhas e o DuckDB tem de os percorrer. Use este método para a exportação, não para um dashboard que seja recarregado 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 converte os períodos encerrados em Parquet. O DuckDB responde a todas as consultas que abrangem vários meses, e a base de dados da aplicação mantém-se pequena, o que torna as escritas mais rápidas.
Execute a exportação segundo um agendamento, em vez de a executar manualmente. Um par de serviço e temporizador do systemd tem a dimensão certa para este caso: uma unidade que executa o COPY e um temporizador que a aciona todas as noites.
Executar ambos no mesmo VPS
Nada neste caso precisa de um contentor e nada precisa de uma porta. Ambos os motores são bibliotecas, por isso a instalação consiste num pacote e num caminho de ficheiro. Se o resto da sua stack já for executado através do Docker Compose no mesmo VPS, monte o diretório de dados no contentor que precisa dele em vez de adicionar um serviço de base de dados, porque não existe nenhum serviço para adicionar.
Duas regras mantêm esta configuração fora de problemas.
Dê a cada motor o seu próprio diretório: /srv/app para o ficheiro SQLite que a aplicação grava e /srv/data para os ficheiros Parquet que o sistema de análise lê. Quando partilham um diretório, uma tarefa de backup que cria um snapshot de um deles acaba por entrar em conflito com o outro.
Não aponte dois processos para o mesmo ficheiro de base de dados DuckDB em modo de leitura e escrita. Apenas um processo pode manter um ficheiro DuckDB aberto para escrita, e o segundo falha logo ao tentar abri-lo. É possível ter muitos leitores quando todos definem access_mode = 'READ_ONLY'. Isto surpreende quem vem do SQLite, onde é habitual vários processos partilharem um ficheiro. Se o seu sistema de análise apenas lê ficheiros Parquet, esta questão nunca surge, o que constitui mais uma razão para manter o estado persistente no SQLite.
Os backups diferem, e a diferença causa problemas
Uma base de dados SQLite em execução é composta por três ficheiros. Copiá-los com cp durante uma operação de escrita produz um ficheiro que abre, mas está incorreto. Use o comando de backup do próprio mecanismo. Ele cria um snapshot consistente enquanto a aplicação continua a escrever:
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 numa cópia válida. Qualquer outro resultado significa que deve eliminar esse snapshot e criar outro.
Os ficheiros Parquet nunca são alterados depois de escritos, por isso não requerem tratamento especial: faça backup do diretório. Envie ambos os caminhos para fora do servidor com backups restic a partir do seu VPS. Assim, toda a camada de dados fica contida em dois diretórios num único job de backup.
Modos de falha e as mensagens exatas que verá
database is locked do SQLite, quer surja através do driver da sua aplicação quer da shell sqlite3, significa que outra ligação manteve o bloqueio de escrita durante mais tempo do que o permitido pelo seu timeout. Não é corrupção. Defina PRAGMA busy_timeout em todas as ligações e procure depois uma transação longa que deveria ter sido dividida em várias transações curtas.
unable to open database file depois de uma alteração de permissões normalmente significa que o processo consegue escrever no ficheiro, mas não no diretório. O SQLite cria app.db-wal e app.db-shm junto à base de dados, por isso o próprio diretório tem de permitir escrita, não apenas o ficheiro .db.
IO Error: Could not set lock on file do DuckDB significa que outro processo já tem essa base de dados aberta para escrita. Feche a outra shell ou abra a sua em modo somente leitura.
Out of Memory Error do DuckDB numa VPS pequena significa que uma consulta precisou de mais memória de trabalho do que a disponível. Por predefinição, o DuckDB grava no disco os resultados intermédios quando precisa de libertar memória. Isto também se aplica a uma sessão :memory:, por isso abrir um ficheiro de base de dados não é o que permite usar o spill. Esses ficheiros temporários são gravados num diretório .tmp junto ao ficheiro da base de dados ou, numa sessão em memória, num diretório .tmp relativo ao local onde iniciou a sessão. Inicie-a num local com permissões de escrita e espaço livre em disco ou indique outro local com SET temp_directory = '/var/tmp/duckdb';. A definição importante é o limite: SET memory_limit = '2GB';. O valor predefinido é 80% da RAM da máquina. Num servidor que execute outros serviços, esse limite impede que uma consulta ad hoc expulse a aplicação da RAM. Alguns agregados, como string_agg(), não podem usar spill. O mesmo acontece com alguns agregados baseados em ordenação. Por isso, uma consulta que os utilize ainda pode ficar sem memória.
Binder Error: Referenced column "amount" not found ao consultar Parquet quase sempre significa que o schema do ficheiro não é o que tinha em mente. 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 escrita. Muitas escritas pequenas que têm de sobreviver a uma falha de energia significam que SQLite é a escolha adequada. Pergunte qual é o padrão de leitura. Varreduras completas com agregações sobre um histórico longo significam que DuckDB é a escolha adequada. A maioria dos sistemas reais responde afirmativamente às duas perguntas. A resposta correta é atribuir a cada mecanismo a parte em que é mais adequado, em vez de obrigar um deles a substituir o outro.
A migração a evitar é mover o estado ativo da aplicação para DuckDB porque um relatório estava lento. O relatório estava lento por causa da organização do armazenamento. Por isso, a correção é uma exportação, não uma reescrita do caminho de escrita.
FAQ
O DuckDB pode substituir o SQLite pela base de dados da minha aplicação?
Não para uma aplicação que escreve com frequência. O DuckDB bloqueia a escrita de todo o ficheiro da base de dados, permite apenas um processo de leitura e escrita de cada vez e está otimizado para alterações em massa, não para inserções de uma só 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 do que o SQLite para análises?
Para varrimentos e agregações sobre uma tabela grande, sim. A razão é a organização do armazenamento, não um ajuste de configuração. O DuckDB lê apenas as colunas indicadas pela consulta e processa os valores em lotes, enquanto o SQLite tem de percorrer as linhas completas para chegar a um campo. Para obter uma única linha pela chave primária, a ordem inverte-se, porque o SQLite acede a duas páginas e o DuckDB acede ao armazenamento de todas as colunas.
Preciso de muita RAM para executar o DuckDB numa VPS?
Não, mas defina um limite e reserve espaço para o spill. Por predefinição, o DuckDB grava resultados intermédios no disco, mesmo numa sessão :memory:, desde que o diretório temporário tenha permissões de escrita e espaço livre. Depois, defina SET memory_limit = '2GB'; com um valor que a sua VPS possa disponibilizar. Se ficar no valor predefinido de 80% da RAM, um GROUP BY grande pode provocar Out of Memory Error ou expulsar outros serviços da RAM.
Como passo os meus dados do SQLite para Parquet?
Ligue o ficheiro SQLite a partir do DuckDB e copie diretamente uma consulta com COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Execute-a segundo um agendamento para períodos fechados, como as linhas do mês passado, e mantenha as linhas recentes no SQLite, onde a aplicação continua a escrever.
Qual dos dois devo incluir nas cópias de segurança e como?
Ambos, de formas diferentes. Crie snapshots do SQLite com sqlite3 app.db ".backup '/srv/backup/app.db'" em vez de cp, porque uma base de dados em execução também é um -wal e um ficheiro -shm, e uma cópia simples pode ficar inconsistente. Os ficheiros Parquet nunca mudam depois de serem escritos, por isso basta copiar o diretório.