SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-07

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/app

Crie 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);
SQL

A 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çar por "quantos", "quanto" ou "quais os dez primeiros", e a entrada for um conjunto de ficheiros CSV ou Parquet. Instale o cliente de linha de comandos, 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 mostra 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 diretamente o ficheiro, 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 valor em .timer em vez de confiar num valor publicado, porque o resultado depende do seu disco e do número de cores. O que importa é a ordem de grandeza. Não houve CREATE TABLE, INSERT nem uma etapa de carregamento: o DuckDB leu o footer do Parquet, determinou de que chunks de colunas a consulta precisava e leu apenas esses chunks. Um diretório completo funciona da mesma forma com um glob, FROM '/srv/data/orders-*.parquet', que transforma um mês de exportações diárias numa única consulta.

A velocidade do disco estabelece 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 strings exatas que verá

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

Error: 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 respetivo 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, e 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 apenas de 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. O DuckDB usa o disco quando consegue, por isso dê-lhe um local para fazer spill abrindo uma base de dados num ficheiro em disco em vez de :memory: e limite o consumo com SET memory_limit = '2GB';. Num 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 schema do ficheiro não é o que recorda. 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 em 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 ú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 for necessário gerar um relatório.

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

Para varrimentos e agregações numa tabela grande, sim. A razão é a organização do armazenamento, não um truque de otimização. O DuckDB lê apenas as colunas indicadas pela consulta e processa os valores em lotes, enquanto o SQLite tem de percorrer linhas completas para chegar a um campo. Ao obter uma única linha pela chave primária, a relação 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 disponibilize espaço em disco. Abra um ficheiro de base de dados em vez de :memory: para que o DuckDB possa descarregar resultados intermédios para o disco. Depois, defina SET memory_limit = '2GB'; com um valor que a sua VPS possa disponibilizar. Sem um limite, um GROUP BY grande pode aumentar Out of Memory Error ou expulsar outros serviços da RAM.

Como transfiro os meus dados do SQLite para Parquet?

Associe 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 esse processo segundo um agendamento para períodos encerrados, 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 salvaguardar 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 -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.