SQL VACUUM e ANALYZE

A Ana guardou um histórico de 20 mil visitas ao site da loja dentro do loja.db, e o arquivo passou de 45 KB para mais de 1 MB. Depois ela apagou a tabela inteira... e o arquivo continuou com 1 MB. Onde foi parar o espaço? Nesta aula você vai ver dois comandos de manutenção do banco: o VACUUM ("aspirador de pó"), que arruma o arquivo por dentro e devolve o espaço vazio, e o ANALYZE ("analise"), que conta como os dados estão distribuídos para o SQLite escolher o caminho mais rápido em cada consulta.

Você vai aprender
  • Por que apagar dados não diminui o arquivo do banco, e como ver o espaço livre com freelist_count.
  • O VACUUM: o que ele faz, quanto o arquivo diminui (com o tamanho real, antes e depois) e quando usar.
  • O auto_vacuum, que devolve o espaço sozinho, e por que ele vem desligado.
  • O ANALYZE e a tabela sqlite_stat1, onde ficam as estatísticas.
  • O PRAGMA optimize, o jeito recomendado de manter as estatísticas em dia.
  • As mensagens reais quando a manutenção dá errado.

O tamanho de um arquivo só existe no disco, então as medidas desta aula saíram do terminal da Ana, com o ls -l antes e depois de cada passo. Mas os dois comandos rodam também no editor do guia: lá dá para ver as páginas livres e as estatísticas do banco em memória, nos quadros "Resultado".


A ideia: o fichário com fichas em branco

Imagine o fichário de clientes de uma loja, com folhas numeradas. Quando um cliente sai, você apaga a ficha dele, mas a folha continua no fichário, em branco, esperando a ficha de um cliente novo. O fichário não fica mais fino. Para ele ficar fino de novo, alguém precisa tirar todas as fichas, jogar fora as folhas em branco e montar o fichário de novo, com as fichas bem juntinhas.

No fichárioNo banco
Uma folhaUma página do arquivo (4096 bytes no loja.db; veja PRAGMAs Úteis)
Folhas em branco, esperando ficha novaA lista de páginas livres (freelist): PRAGMA freelist_count
Remontar o fichário sem as folhas em brancoVACUUM
Uma etiqueta "nesta gaveta há uns 30 clientes de Teresina"As estatísticas do ANALYZE, na tabela sqlite_stat1

Por que o banco não joga fora as páginas vazias na hora? Porque é mais rápido reaproveitá-las: o próximo INSERT usa uma página da lista em vez de aumentar o arquivo. Na maioria dos bancos, esse é o comportamento padrão.


1. O espaço que não volta

Antes de começar, a Ana mede o arquivo e confere quantas páginas ele tem, e se a opção auto_vacuum (que você vai ver na seção 4) está ligada:

$ ls -l loja.db
-rw-r--r-- 1 ana ana 45056 out 18 11:14 loja.db
$ sqlite3 loja.db "PRAGMA auto_vacuum; PRAGMA page_count;"
0
11

São 45056 bytes: 11 páginas de 4096. O auto_vacuum está em 0, desligado, que é o padrão do SQLite para bancos novos e o que você vai encontrar na maioria dos bancos.

Agora o histórico de visitas: uma tabela nova com 20 mil linhas, criadas de uma vez com um WITH RECURSIVE que conta de 1 a 20000 (veja CTE Recursiva):

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> CREATE TABLE historico (id INTEGER PRIMARY KEY, evento TEXT);
sqlite> WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 20000) INSERT INTO historico SELECT i, 'visita ao produto ' || (i % 45 + 1) || ' registrada pelo site da loja' FROM n;
sqlite> .quit
$ ls -l loja.db
-rw-r--r-- 1 ana ana 1200128 out 18 11:21 loja.db

De 45 KB para 1,2 MB. A Ana desiste do histórico e apaga a tabela inteira:

$ sqlite3 loja.db "DROP TABLE historico;"
$ ls -l loja.db
-rw-r--r-- 1 ana ana 1200128 out 18 11:21 loja.db
$ sqlite3 loja.db "PRAGMA page_count; PRAGMA freelist_count;"
293
282
1200128
O arquivo continua exatamente do mesmo tamanho, mesmo sem a tabela.
page_count = 293
O arquivo tem 293 páginas (293 × 4096 = 1200128).
freelist_count = 282
Dessas, 282 estão livres: eram do histórico e agora estão em branco. Só 11 páginas têm dados de verdade, as mesmas 11 do começo.

Mais de 96% do arquivo está vazio. Os dados da loja estão intactos; só o espaço não voltou para o disco.


2. VACUUM: remontar o arquivo

É aqui que entra o aspirador:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> .timer on
sqlite> VACUUM;
Run Time: real 0.013 user 0.001175 sys 0.000000
sqlite> .quit
$ ls -l loja.db
-rw-r--r-- 1 ana ana 45056 out 18 11:21 loja.db
$ sqlite3 loja.db "PRAGMA page_count; PRAGMA freelist_count;"
11
0
$ sqlite3 loja.db "SELECT count(*) FROM itens_pedido;"
220
.timer on
Faz o sqlite3 mostrar quanto tempo cada comando levou (aqui, 13 milésimos de segundo, num banco pequeno).
VACUUM;
Copia todo o conteúdo do banco para um arquivo temporário, bem arrumado, sem páginas vazias, e depois põe essa cópia no lugar do original.
45056
O arquivo voltou ao tamanho do começo: de 1,2 MB para 44 KB.
11 e 0
Onze páginas, nenhuma livre.
220
Os dados continuam todos lá. O VACUUM só arruma; não apaga nada.

Além de devolver o espaço, o VACUUM põe as páginas de cada tabela em sequência no arquivo. Num banco que passou anos recebendo e apagando dados, as páginas de uma tabela ficam espalhadas, como um fichário bagunçado, e ler fica um pouco mais lento. Depois do VACUUM, tudo fica em ordem.

O mesmo acontece no editor do guia, com o banco em memória. Lá o histórico ocupa menos páginas, porque o banco do editor usa páginas de 8192 bytes:

CREATE TABLE historico (id INTEGER PRIMARY KEY, evento TEXT);
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 20000)
INSERT INTO historico
SELECT i, 'visita ao produto ' || (i % 45 + 1) || ' registrada pelo site da loja' FROM n;

SELECT page_count, freelist_count FROM pragma_page_count, pragma_freelist_count;

DROP TABLE historico;
SELECT page_count, freelist_count FROM pragma_page_count, pragma_freelist_count;

VACUUM;
SELECT page_count, freelist_count FROM pragma_page_count, pragma_freelist_count;

Os três SELECT contam a história inteira: 150 páginas com o histórico; 150 páginas, 141 delas livres, depois do DROP; e 9 páginas, nenhuma livre, depois do VACUUM. O FROM pragma_page_count, pragma_freelist_count junta dois PRAGMAs numa linha só, com a forma de função que você viu em PRAGMAs Úteis.

Experimente: a Ana apagou os 20 mil registros com DELETE (em vez de DROP TABLE), e o último SELECT mostra as páginas livres que sobraram. Acrescente o comando que devolve o espaço, antes do último SELECT, para que ele mostre 10 páginas e 0 livres (uma página a mais que no exemplo de cima: a tabela historico, vazia, continua existindo).

CREATE TABLE historico (id INTEGER PRIMARY KEY, evento TEXT);
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 20000)
INSERT INTO historico
SELECT i, 'visita ao produto ' || (i % 45 + 1) || ' registrada pelo site da loja' FROM n;

DELETE FROM historico;

SELECT page_count, freelist_count FROM pragma_page_count, pragma_freelist_count;
CREATE TABLE historico (id INTEGER PRIMARY KEY, evento TEXT);
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 20000)
INSERT INTO historico
SELECT i, 'visita ao produto ' || (i % 45 + 1) || ' registrada pelo site da loja' FROM n;

DELETE FROM historico;

VACUUM;
SELECT page_count, freelist_count FROM pragma_page_count, pragma_freelist_count;

3. Quando usar o VACUUM (e quando não)

O VACUUM é seguro, mas não é de graça. Para remontar o arquivo, ele:

  • lê e reescreve o banco inteiro: num banco de 13 milésimos de segundo, nada; num banco de vários gigabytes, pode levar minutos;
  • precisa de espaço livre no disco: a documentação do SQLite avisa que ele pode usar até o dobro do tamanho do banco, entre a cópia temporária e o diário;
  • trava o banco enquanto trabalha: nenhum outro programa consegue escrever, e ele não roda dentro de uma transação (veja Quando dá errado).
SituaçãoVale um VACUUM?
Você apagou uma parte grande dos dados (uma tabela de log, um ano de histórico) e quer o espaço de voltaSim
Vai mandar o banco para alguém, ou guardar uma cópiaSim, ou melhor, VACUUM INTO 'copia.db', que já gera a cópia compactada (aula Backup)
Quer mudar o page_size ou o auto_vacuum de um banco que já existeSim: a mudança só vale depois de um VACUUM
Apagou umas poucas linhasNão: as páginas livres serão reaproveitadas pelos próximos INSERT
"Toda noite, por garantia", num banco que só cresceNão: é trabalho sem resultado
Os rowid podem mudar

Toda tabela do SQLite tem um número escondido por linha, o rowid. Numa tabela com INTEGER PRIMARY KEY (como todas as da Loja Aurora), o id é o rowid, e o VACUUM nunca o muda. Mas numa tabela sem essa coluna, o VACUUM pode renumerar os rowid. Se algum programa guarda rowid de uma tabela assim, ele vai se perder. Mais um motivo para toda tabela ter a sua chave primária (veja PRIMARY KEY).


4. auto_vacuum: o espaço volta sozinho

Existe um jeito de o espaço voltar sozinho, sem VACUUM: a opção auto_vacuum. Num banco que já tem tabelas, mudar essa opção só vale depois de um VACUUM, que remonta o arquivo no modo novo. A Ana liga a opção (FULL = "completo") e repete o teste do histórico:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> PRAGMA auto_vacuum = FULL;
sqlite> VACUUM;
sqlite> PRAGMA auto_vacuum;
1
sqlite> CREATE TABLE historico (id INTEGER PRIMARY KEY, evento TEXT);
sqlite> WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 20000) INSERT INTO historico SELECT i, 'visita ao produto ' || (i % 45 + 1) || ' registrada pelo site da loja' FROM n;
sqlite> .quit
$ ls -l loja.db
-rw-r--r-- 1 ana ana 1204224 out 18 11:21 loja.db
$ sqlite3 loja.db "DROP TABLE historico;"
$ ls -l loja.db
-rw-r--r-- 1 ana ana 49152 out 18 11:21 loja.db

Desta vez o arquivo diminuiu na hora, sem VACUUM: no fim de cada transação, o SQLite move as páginas livres para o fim do arquivo e corta o arquivo ali. Repare que, com a opção ligada, o banco ficou com 49152 bytes, uma página a mais que os 45056 do começo: é o "mapa" que o SQLite mantém para saber o que pode mover.

PRAGMA auto_vacuumO que fazBom para
0 / NONEPáginas livres ficam no arquivo, para reaproveitar; só o VACUUM devolve. É o padrão para bancos novosA maioria dos casos: escrever é mais rápido
1 / FULLDevolve as páginas livres no fim de cada transaçãoCelulares e aparelhos com pouco espaço
2 / INCREMENTALGuarda o mapa, mas só devolve quando você pede: PRAGMA incremental_vacuum(N); devolve até N páginasQuem quer escolher a hora da limpeza

Atenção: o auto_vacuum devolve o espaço, mas não arruma a ordem das páginas, e com o tempo pode até deixar o arquivo mais bagunçado. O VACUUM continua sendo o único jeito de remontar o arquivo por completo.


5. ANALYZE: as estatísticas

Agora o outro comando da aula, que não mexe no tamanho do arquivo. Para cada consulta, o SQLite escolhe um caminho: ler a tabela inteira, ou usar um índice (aula EXPLAIN QUERY PLAN). Para escolher bem, ele precisa saber como os dados estão distribuídos: quantas linhas a tabela tem, quantas linhas cada valor de um índice costuma ter. O ANALYZE conta isso e guarda o resultado numa tabela especial, a sqlite_stat1:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> SELECT * FROM sqlite_stat1;
Parse error: no such table: sqlite_stat1
sqlite> ANALYZE;
sqlite> SELECT * FROM sqlite_stat1;
itens_pedido|sqlite_autoindex_itens_pedido_1|220 3 1
pedidos||90
funcionarios||8
clientes||30
categorias||8
fornecedores||6
produtos||45
no such table: sqlite_stat1
Antes do primeiro ANALYZE, a tabela de estatísticas nem existe.
ANALYZE;
Lê as tabelas e os índices do banco e cria a sqlite_stat1. Num banco grande, pode levar um tempo; dá para analisar só uma tabela: ANALYZE produtos;.
tbl | idx | stat
As três colunas da sqlite_stat1: a tabela, o índice (vazio quando a linha fala da tabela) e os números.
pedidos||90
A tabela pedidos tem 90 linhas.
220 3 1
Sobre o índice da chave (pedido_id, produto_id): a tabela tem 220 linhas; cada valor de pedido_id aparece, em média, em umas 3 linhas (são 2 ou 3 itens por pedido); e cada par (pedido_id, produto_id), em 1 linha só.

Com um índice novo, a conta aparece para ele também. A Ana cria um índice de produtos por categoria e analisa de novo, agora no editor:

CREATE INDEX idx_produtos_categoria ON produtos (categoria_id);
ANALYZE;

SELECT tbl, idx, stat FROM sqlite_stat1 ORDER BY tbl;

A linha nova, produtos | idx_produtos_categoria | 45 6, diz: 45 produtos, e cada categoria tem, em média, uns 6 (45 divididos por 8 categorias dá 5,6, arredondado para cima). Com esse número, o SQLite sabe que procurar "os produtos da categoria 2" pelo índice vai achar umas 6 linhas, bem menos que ler as 45. Numa tabela de milhões de linhas, uma estimativa boa como essa é o que separa uma consulta de um segundo de uma de um minuto.

O sqlite_stat1 é para ler, não para mexer

Ela é uma tabela de verdade, e um SELECT nela funciona. Mas quem escreve nela é o ANALYZE. Se as estatísticas ficarem velhas, a solução é rodar o ANALYZE de novo, nunca editar os números à mão.


6. PRAGMA optimize: estatísticas em dia, sem esforço

Quando rodar o ANALYZE? Depois de criar um índice, ou quando os dados mudarem muito. Para não ter de pensar nisso, existe o PRAGMA optimize: ele confere o que mudou e só roda o ANALYZE nas tabelas em que vale a pena. A Ana testa com um índice recém-criado:

sqlite> DROP INDEX idx_produtos_categoria;
sqlite> CREATE INDEX idx_produtos_categoria ON produtos (categoria_id);
sqlite> SELECT * FROM sqlite_stat1 WHERE tbl = 'produtos';
sqlite> PRAGMA optimize;
sqlite> SELECT * FROM sqlite_stat1 WHERE tbl = 'produtos';
produtos|idx_produtos_categoria|45 6
sqlite> EXPLAIN QUERY PLAN SELECT nome FROM produtos WHERE categoria_id = 2;
QUERY PLAN
`--SEARCH produtos USING INDEX idx_produtos_categoria (categoria_id=?)

Logo depois do CREATE INDEX, o índice ainda não tinha estatística (o primeiro SELECT não mostrou nada). O PRAGMA optimize percebeu o índice novo e analisou a tabela: a linha 45 6 apareceu. E o EXPLAIN QUERY PLAN mostra o SQLite usando o índice para achar os produtos da categoria 2 (veja Quando o Índice Ajuda).

A documentação do SQLite recomenda rodar o PRAGMA optimize antes de fechar cada conexão (e, em programas que ficam muito tempo abertos, de tempos em tempos). Na maioria das vezes ele não faz nada e termina num instante; quando há o que fazer, faz só o necessário.


7. Formas diferentes de fazer

ComandoO que fazQuando usar
VACUUM;Remonta o arquivo: devolve o espaço livre e põe as páginas em ordemDepois de apagar muitos dados; para aplicar page_size/auto_vacuum
VACUUM INTO 'copia.db';O mesmo, mas grava o resultado num arquivo novoBackup compactado; o original não é tocado
PRAGMA auto_vacuum = FULL; + VACUUM;O espaço volta sozinho a cada transaçãoPouco espaço em disco
PRAGMA incremental_vacuum(N);Devolve até N páginas (com auto_vacuum = INCREMENTAL)Limpar aos poucos, na hora que você escolhe
ANALYZE; / ANALYZE tabela;Recalcula as estatísticas (de tudo ou de uma tabela)Depois de criar índices ou de grandes mudanças
PRAGMA optimize;Roda o ANALYZE só onde vale a penaSempre, antes de fechar a conexão

Quando dá errado

VACUUM dentro de uma transação
BEGIN;
DELETE FROM itens_pedido WHERE pedido_id = 90;
VACUUM;

O que aconteceu: "não posso fazer VACUUM de dentro de uma transação". O VACUUM troca o arquivo inteiro, e isso não cabe dentro de uma transação que ainda pode ser desfeita. No terminal, a mensagem é a mesma, com o prefixo: Runtime error: cannot VACUUM from within a transaction.

Como consertar: termine a transação (COMMIT ou ROLLBACK) e rode o VACUUM sozinho, depois.

Ler a sqlite_stat1 antes do ANALYZE
SELECT * FROM sqlite_stat1;

O que aconteceu: "não existe a tabela sqlite_stat1". Ela só é criada pelo primeiro ANALYZE (ou por um PRAGMA optimize que decidiu analisar algo). No editor, como o banco é recriado a cada Rodar, ela nunca existe no começo.

Como consertar: rode ANALYZE; antes, na mesma caixa.

Mudou o auto_vacuum e nada aconteceu
PRAGMA auto_vacuum = FULL;
PRAGMA auto_vacuum;

VACUUM;
PRAGMA auto_vacuum;

O que aconteceu: num banco que já tem tabelas, PRAGMA auto_vacuum = FULL; não dá erro, mas também não muda o modo: a primeira leitura continua mostrando 0. Só depois do VACUUM, que remonta o arquivo, a leitura mostra 1. Foi por isso que, na seção 4, a Ana rodou o VACUUM logo depois de mudar a opção.

Como evitar: como todo PRAGMA (aula PRAGMAs Úteis), confira lendo de volta: PRAGMA auto_vacuum;.

Disco cheio no meio do VACUUM

Se não houver espaço para a cópia temporária, o VACUUM falha e o banco fica como estava, sem perder nada (é uma transação como outra qualquer; lembre do "tudo ou nada" da aula ACID). Libere espaço no disco, ou use VACUUM INTO para gravar a versão compactada em outro disco.


Nos outros bancos

Manutenção no MySQL, PostgreSQL e SQL Server

Todo banco precisa dessas duas tarefas, devolver espaço e atualizar estatísticas, mas cada um tem os seus comandos (trechos ilustrativos):

-- PostgreSQL: VACUUM existe, com outro sentido; ANALYZE também
VACUUM produtos;          -- marca o espaço para reaproveitar
VACUUM FULL produtos;     -- reescreve a tabela e devolve o espaço (como o nosso VACUUM)
ANALYZE produtos;

-- MySQL
OPTIMIZE TABLE produtos;  -- reorganiza e devolve o espaço
ANALYZE TABLE produtos;   -- estatísticas

-- SQL Server
UPDATE STATISTICS produtos;

No PostgreSQL, um processo do próprio servidor, o autovacuum, roda o VACUUM e o ANALYZE sozinho, o tempo todo. Nos bancos com servidor, quem cuida dessa manutenção costuma ser o administrador do banco; no SQLite, o banco é seu, então essa tarefa também é sua.


Referência rápida

ComandoO que faz
VACUUM;Remonta o arquivo do banco, sem páginas livres
VACUUM INTO 'arquivo.db';Grava a versão remontada num arquivo novo
PRAGMA freelist_count;Quantas páginas estão livres
PRAGMA page_count;Quantas páginas o arquivo tem
PRAGMA auto_vacuum;0 (NONE), 1 (FULL) ou 2 (INCREMENTAL); mudar exige um VACUUM
PRAGMA incremental_vacuum(N);Devolve até N páginas livres (modo INCREMENTAL)
ANALYZE; / ANALYZE tabela;Calcula as estatísticas e as guarda em sqlite_stat1
SELECT * FROM sqlite_stat1;Mostra as estatísticas: tabela, índice, números
PRAGMA optimize;Atualiza as estatísticas só onde precisa
MensagemQuer dizer
cannot VACUUM from within a transactionHá um BEGIN aberto: termine a transação antes
no such table: sqlite_stat1O ANALYZE ainda não foi rodado neste banco

Exercícios

1. Quanto espaço está livre?

Complete o PRAGMA que conta as páginas livres do banco:

PRAGMA ;

2. A cópia compactada

Complete para gravar uma versão remontada do banco no arquivo pequeno.db, sem mexer no original:

VACUUM 'pequeno.db';

3. Num banco com auto_vacuum desligado, a Ana apagou uma tabela de 1 MB. O que acontece com o tamanho do arquivo?

4. O que o ANALYZE faz?

5. A Ana rodou VACUUM logo depois de um BEGIN. O que acontece?

6. Qual é o jeito recomendado de manter as estatísticas em dia sem pensar nelas?

Missão final: a estatística do índice novo

Missão: a Ana criou um índice de clientes por cidade, mas a consulta à sqlite_stat1 dá erro: as estatísticas ainda não existem. Acrescente o comando que as calcula, no lugar certo. A linha do índice deve mostrar 30 4: 30 clientes, uns 4 por cidade.

CREATE INDEX idx_clientes_cidade ON clientes (cidade);

SELECT idx, stat FROM sqlite_stat1 WHERE tbl = 'clientes';
CREATE INDEX idx_clientes_cidade ON clientes (cidade);
ANALYZE;

SELECT idx, stat FROM sqlite_stat1 WHERE tbl = 'clientes';

São 8 cidades diferentes para 30 clientes: 30 ÷ 8 = 3,75, arredondado para cima, 4. O ANALYZE clientes;, só da tabela, também resolve. Teresina, com 10 clientes, tem bem mais que a média; a sqlite_stat1 guarda só a média, e para a maioria das consultas isso basta.


Resumo

  • Apagar dados não diminui o arquivo: as páginas ficam livres para reaproveitar. PRAGMA freelist_count mostra quantas.
  • VACUUM remonta o arquivo, devolve o espaço e põe as páginas em ordem, sem apagar nenhum dado. No teste da Ana: de 1200128 para 45056 bytes.
  • O VACUUM reescreve o banco inteiro, pode precisar do dobro do espaço e não roda dentro de transação. Use depois de apagar muitos dados, não "por garantia".
  • auto_vacuum = FULL devolve o espaço sozinho a cada transação; o padrão é NONE. Mudar o modo exige um VACUUM.
  • ANALYZE calcula as estatísticas (linhas por tabela, linhas por valor de índice) e as guarda em sqlite_stat1; o SQLite as usa para escolher o caminho das consultas.
  • PRAGMA optimize, antes de fechar a conexão, mantém as estatísticas em dia fazendo só o necessário.