SQL Backup: .dump e .backup

Todo o cadastro da Loja Aurora (clientes, produtos, 90 pedidos) mora num arquivo só: ~/loja.db. Se o notebook da Ana quebrar, ou se alguém rodar um DELETE sem WHERE, a loja perde tudo. A proteção é uma cópia de segurança, o backup. Nesta aula você vai ver os dois jeitos que o sqlite3 oferece: o .dump, que transforma o banco num arquivo de SQL que qualquer pessoa consegue ler, e o .backup, que faz uma cópia exata do arquivo do banco. E vai ver como trazer os dados de volta.

Você vai aprender
  • Por que fazer backup, e por que copiar o arquivo com o banco aberto pode dar errado.
  • O .dump: o banco inteiro (ou uma tabela) escrito como comandos SQL.
  • A salvar o dump num arquivo (sqlite3 loja.db .dump > backup.sql) e a recriar o banco a partir dele.
  • O .backup e o .restore: cópia exata do banco e volta da cópia.
  • O VACUUM INTO, que faz a cópia com um comando SQL.
  • As armadilhas reais: restaurar por cima de um banco cheio e o .restore que apaga tudo.

Como a aula anterior, Importar e Exportar CSV, esta é uma aula de terminal: backup é gravar arquivos, e o editor do guia não grava arquivos no seu computador. As sessões saíram do terminal da Ana, na pasta pessoal dela (~), onde está o loja.db. O editor entra para mostrar que um dump é SQL comum, que roda em qualquer lugar.


A ideia: a foto e a receita

Imagine que a Ana quer guardar uma cópia de um bolo que deu muito certo. Ela tem dois jeitos:

JeitoNa cozinhaNo banco
A receitaAnota cada ingrediente e cada passo. Qualquer pessoa lê e faz o bolo de novo, em qualquer cozinha..dump: um arquivo de texto com os CREATE TABLE e os INSERT que recriam o banco.
O congeladorGuarda um bolo igualzinho no freezer. É só descongelar, sem refazer nada..backup: uma cópia exata do arquivo .db, pronta para abrir.

A receita é ótima para ler, comparar e levar para outro banco. O congelador é mais rápido de fazer e de usar. Muita gente guarda os dois.

E o cp loja.db copia.db?

Copiar o arquivo com o cp funciona quando nenhum programa está mexendo no banco. Se um programa estiver gravando bem na hora da cópia, a cópia pode sair com metade de uma alteração, e virar um arquivo estragado. Além disso, num banco no modo WAL (veja PRAGMAs Úteis), parte dos dados recentes fica num segundo arquivo, o loja.db-wal, que o cp de um arquivo só deixa para trás. O .backup e o VACUUM INTO pedem licença ao banco e copiam um retrato consistente. Prefira os dois.


1. O .dump: o banco virado SQL

Comece por uma tabela pequena. Com o nome de uma tabela, o .dump mostra só ela:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> .dump categorias
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE categorias (
    id        INTEGER PRIMARY KEY,
    nome      TEXT NOT NULL,
    descricao TEXT
);
INSERT INTO categorias VALUES(1,'Papelaria','Material escolar e de escritório');
INSERT INTO categorias VALUES(2,'Eletrônicos','Acessórios e dispositivos eletrônicos');
INSERT INTO categorias VALUES(3,'Casa e Cozinha','Utensílios para o lar');
INSERT INTO categorias VALUES(4,'Livros','Livros, dicionários e guias');
INSERT INTO categorias VALUES(5,'Brinquedos','Brinquedos e jogos para todas as idades');
INSERT INTO categorias VALUES(6,'Esporte e Lazer','Artigos esportivos e de lazer');
INSERT INTO categorias VALUES(7,'Alimentos','Alimentos não perecíveis e regionais');
INSERT INTO categorias VALUES(8,'Jardim','Ferramentas e artigos de jardinagem');
COMMIT;
sqlite> .quit

Tudo o que saiu é SQL que você já conhece (ou vai conhecer). Leia de cima para baixo:

PRAGMA foreign_keys=OFF;
Desliga a conferência de chaves estrangeiras durante a restauração. Assim a ordem das tabelas não importa: um produto pode entrar antes da sua categoria. (PRAGMAs têm aula própria: PRAGMAs Úteis.)
BEGIN TRANSACTION;
Abre uma transação: tudo até o COMMIT vale como uma coisa só, e fica muito mais rápido. Você vai ver isso em Transações.
CREATE TABLE categorias (…);
A estrutura da tabela, do jeito que foi escrita quando ela foi criada, com os espaços e tudo (veja CREATE TABLE).
INSERT INTO categorias VALUES(…);
Uma linha da tabela por INSERT, com os valores na ordem das colunas. Os textos vêm em aspas simples, como manda o SQL.
COMMIT;
Fecha a transação e grava tudo.

Rodar esse texto num banco vazio recria a tabela categorias exatamente como ela é. Essa é toda a ideia do dump.


2. O dump do banco inteiro num arquivo

Sem nome de tabela, o .dump escreve o banco inteiro. Em vez de jogar tudo na tela, a Ana manda para um arquivo, numa linha só do terminal:

$ sqlite3 loja.db .dump > backup.sql
$ ls -l loja.db backup.sql
-rw-rw-r-- 1 ana ana 30423 out 18 11:18 backup.sql
-rw-r--r-- 1 ana ana 45056 out 18 11:14 loja.db
$ head -4 backup.sql
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE categorias (
    id        INTEGER PRIMARY KEY,
$ tail -3 backup.sql
INSERT INTO itens_pedido VALUES(90,15,2,54.29999999999999716);
INSERT INTO itens_pedido VALUES(90,16,1,39.99000000000000198);
COMMIT;
$ grep -c '^INSERT' backup.sql
407                              « 8+6+45+30+8+90+220: todas as linhas de todas as tabelas
sqlite3 loja.db
Abre o banco.
.dump
O comando de ponto, passado direto na linha de comando: o sqlite3 roda, escreve e sai.
> backup.sql
O shell grava tudo o que sairia na tela no arquivo backup.sql. O .sql no fim do nome é só um costume, para lembrar que o arquivo é SQL.

O grep -c '^INSERT' conta as linhas que começam com INSERT: 407, exatamente o total de linhas das sete tabelas da loja. O dump ficou menor que o banco (30 KB contra 44 KB): é só texto, sem os índices nem o espaço livre que o arquivo do banco guarda por dentro. Num banco grande, ele também comprime muito bem com gzip.

Que números são esses: 54.29999999999999716?

No banco, o preço desse item é 54.3. O dump escreve o número com todas as casas que o computador guarda de verdade. Números com vírgula (o tipo REAL) são guardados em binário, e 54.3 não tem representação exata em binário, assim como 1/3 não tem em decimal (0,333…). O dump escreve a versão longa para que, ao restaurar, volte exatamente o mesmo número, sem nenhuma diferença:

SELECT preco_unitario
FROM itens_pedido
WHERE pedido_id = 90 AND produto_id = 15;

SELECT 54.29999999999999716 = 54.3 AS igual;

O 1 quer dizer "verdadeiro": para o SQLite, os dois números são o mesmo. Mais sobre essas casas "feias" em Funções Numéricas.


3. Restaurar um dump

Para recriar o banco a partir da "receita", basta dar o arquivo de SQL para um sqlite3 com um banco novo. O < do shell faz o caminho contrário do >: lê o arquivo e entrega ao programa, como se você digitasse tudo:

$ sqlite3 restaurado.db < backup.sql
$ ls -l restaurado.db
-rw-r--r-- 1 ana ana 45056 out 18 11:18 restaurado.db
$ sqlite3 restaurado.db "SELECT count(*) FROM produtos; SELECT count(*) FROM itens_pedido;"
45
220

O restaurado.db não existia: o sqlite3 criou o arquivo vazio, rodou os 407 INSERT e os sete CREATE TABLE, e pronto. As contagens batem com o original, e até o tamanho bateu: 45056 bytes, igual ao loja.db. Faz sentido, porque o loja.db da Ana foi criado exatamente assim, rodando um arquivo de SQL num banco vazio. O tamanho nem sempre bate (um banco que já teve muitos DELETE guarda espaço vazio que o dump não leva, como você vai ver em VACUUM e ANALYZE), e não precisa: o que importa é que os dados são os mesmos.

De dentro do sqlite3, o mesmo efeito vem do .read, que lê e roda um arquivo de SQL:

$ sqlite3 novo.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> .read backup.sql
sqlite> SELECT count(*) FROM clientes;
30
sqlite> .quit
O dump é SQL, então roda em qualquer lugar

Como o dump é só texto com SQL, você pode abrir no editor de texto, procurar um cliente com grep, comparar dois backups com diff para ver o que mudou, ou guardar no Git. Com poucos ajustes (os tipos e o PRAGMA do começo), ele serve até para levar os dados para outro banco, como o PostgreSQL.

E é SQL de verdade: o editor do guia roda um trecho de dump. Aqui, uma tabela que não existe na loja, cupons, escrita no formato do dump:

PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE cupons (codigo TEXT PRIMARY KEY, desconto REAL);
INSERT INTO cupons VALUES('AURORA10',0.1);
INSERT INTO cupons VALUES('FRETEGRATIS',0.0);
COMMIT;

SELECT * FROM cupons;

4. O .backup: a cópia exata

O "congelador" é o .backup. Dentro do sqlite3, ele grava uma cópia idêntica do banco aberto em outro arquivo:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> .backup copia.db
sqlite> SELECT count(*) FROM clientes;
30
.backup
Copia o banco aberto, página por página, usando a "API de backup" do SQLite: enquanto copia, nenhum outro programa consegue deixar o banco pela metade.
copia.db
O arquivo de destino. Se já existir, é substituído sem perguntar.

Logo depois, um acidente: a Ana apaga a cliente 30 sem querer. Como ela tinha acabado de fazer a cópia, o .restore traz tudo de volta:

sqlite> DELETE FROM clientes WHERE id = 30;
sqlite> SELECT count(*) FROM clientes;
29
sqlite> .restore copia.db
sqlite> SELECT count(*) FROM clientes;
30
sqlite> SELECT nome FROM clientes WHERE id = 30;
Rosângela Dias
sqlite> .quit
.restore copia.db
O caminho contrário: substitui o banco aberto pelo conteúdo de copia.db. Tudo o que o banco tinha antes do .restore some, inclusive o que mudou depois do backup.

A Rosângela voltou. Fora do sqlite3, os dois arquivos têm o mesmo tamanho, e o PRAGMA integrity_check ("conferência de integridade") confirma que a cópia está saudável:

$ ls -l loja.db copia.db
-rw-r--r-- 1 ana ana 45056 out 18 11:18 copia.db
-rw-r--r-- 1 ana ana 45056 out 18 11:19 loja.db
$ sqlite3 copia.db "PRAGMA integrity_check;"
ok

Um backup que nunca foi conferido é uma esperança, não um backup. De tempos em tempos, abra a cópia e rode o integrity_check e uma ou duas contagens.

Um backup por dia, com a data no nome

O .backup também funciona numa linha só do terminal. Com o $(date +%F) do shell, que vira a data de hoje no formato ano-mês-dia, cada dia ganha o seu arquivo:

$ sqlite3 loja.db ".backup backup-$(date +%F).db"
$ ls -l backup-*
-rw-r--r-- 1 ana ana 45056 out 18 11:19 backup-2026-10-18.db

As aspas em volta de .backup … fazem o sqlite3 receber o comando inteiro como uma coisa só. Essa linha pode ir para um agendador de tarefas (como o cron, no Linux) e rodar sozinha toda noite.


5. VACUUM INTO: a cópia com SQL

O .backup é um comando de ponto, que só o programa sqlite3 entende. Um programa em Python ou PHP que usa o SQLite precisa de um jeito em SQL. É o VACUUM INTO ("aspire para dentro de"):

$ sqlite3 loja.db "VACUUM INTO 'compacta.db';"
$ ls -l compacta.db
-rw-r--r-- 1 ana ana 45056 out 18 11:19 compacta.db
VACUUM
O comando que reorganiza o banco por dentro e joga fora o espaço vazio (aula VACUUM e ANALYZE).
INTO 'compacta.db'
Em vez de reorganizar o próprio banco, grava o resultado num arquivo novo. O original não é tocado. O nome do arquivo vai entre aspas simples, como um texto.

A cópia sai já "aspirada", sem espaço desperdiçado. Aqui ela ficou do mesmo tamanho do original porque o loja.db está arrumado; num banco que já teve muitos DELETE, a cópia sai menor. Diferente do .backup, o VACUUM INTO não substitui um arquivo que já existe (veja em Quando dá errado).

E no editor do guia?

O VACUUM INTO 'copia.db'; roda no editor e responde "VACUUM: ok". Mas o banco do editor vive na memória do seu navegador, e o arquivo também: não existe a sua pasta pessoal ali, e a cópia some junto com o banco. Backup de verdade, só no terminal.


6. Um backup dentro do próprio banco

Antes de uma mudança arriscada numa tabela só, como um aumento de preços, existe um truque rápido: copiar a tabela para outra tabela no mesmo banco, com CREATE TABLE … AS SELECT (veja Criar Tabela a partir de Consulta). Não substitui o backup em outro arquivo (se o notebook quebrar, as duas tabelas vão junto), mas salva de um UPDATE errado.

Experimente: a Ana guardou os produtos em produtos_backup e logo depois zerou todos os preços por engano. Escreva, no lugar indicado, um UPDATE que devolva a cada produto o preço que está na cópia (dica: SET preco = (SELECT b.preco FROM produtos_backup AS b WHERE b.id = produtos.id)).

CREATE TABLE produtos_backup AS SELECT * FROM produtos;

UPDATE produtos SET preco = 0;   -- o acidente

-- escreva aqui o UPDATE que traz os preços de volta

SELECT nome, preco FROM produtos WHERE id IN (1, 2, 3);
CREATE TABLE produtos_backup AS SELECT * FROM produtos;

UPDATE produtos SET preco = 0;   -- o acidente

UPDATE produtos
SET preco = (SELECT b.preco FROM produtos_backup AS b WHERE b.id = produtos.id);

SELECT nome, preco FROM produtos WHERE id IN (1, 2, 3);

7. Formas diferentes de fazer

FormaO que geraVantagensCuidados
sqlite3 loja.db .dump > backup.sqlTexto com SQLLegível, comparável com diff, serve para outros bancos, comprime bemRestaurar é rodar todo o SQL: num banco grande, demora
.dump tabelaSQL de uma tabelaLeva uma tabela só para outro bancoNão leva as tabelas relacionadas
.backup copia.dbCópia exata do .dbRápido, consistente, pronto para abrirArquivo binário: não dá para ler nem comparar no editor de texto
.restore copia.db(volta a cópia)Desfaz tudo desde o backupApaga o que veio depois; com nome errado, apaga tudo
VACUUM INTO 'copia.db'Cópia compactada do .dbÉ SQL: funciona em qualquer programa que usa SQLiteNão sobrescreve um arquivo que já existe
cp loja.db copia.dbCópia do arquivoSimplesSó com o banco fechado; esquece o -wal
CREATE TABLE x AS SELECT …Tabela de reserva no mesmo bancoInstantâneo, antes de uma mudança arriscadaNão é backup: fica no mesmo arquivo

Qual usar? Para o dia a dia, um .backup com a data no nome, todo dia, guardado em outro lugar (um pendrive, outro computador, a nuvem). De vez em quando, um .dump, que você consegue ler mesmo daqui a dez anos, com qualquer programa.


Quando dá errado

Restaurou o dump por cima do banco cheio

A Ana tenta "restaurar" o backup.sql no próprio loja.db, que ainda tem todas as tabelas:

$ sqlite3 loja.db < backup.sql 2>&1 | head -6
Parse error near line 3: table categorias already exists
  CREATE TABLE categorias (     id        INTEGER PRIMARY KEY,     nome      TEX
               ^--- error here
Runtime error near line 8: UNIQUE constraint failed: categorias.id (19)
Runtime error near line 9: UNIQUE constraint failed: categorias.id (19)
Runtime error near line 10: UNIQUE constraint failed: categorias.id (19)
$ sqlite3 loja.db < backup.sql 2>&1 | wc -l
428

O que aconteceu: "erro perto da linha 3: a tabela categorias já existe", com a seta apontando onde o SQLite parou de entender. Depois, um "a restrição UNIQUE falhou em categorias.id" para cada INSERT, porque cada linha já estava lá (o (19) é o número do código de erro do SQLite para restrição violada). Ao todo, 428 linhas de erro. O 2>&1 | head -6 é só para mostrar as primeiras: junta as mensagens de erro na saída e corta nas 6 primeiras linhas. O banco não estragou (nada entrou duplicado), mas também não foi restaurado.

Como consertar: restaure um dump sempre num banco novo (sqlite3 restaurado.db < backup.sql), confira, e só então troque um arquivo pelo outro.

No editor acontece o mesmo: a Loja Aurora já está carregada, então o CREATE TABLE do dump bate na tabela que existe:

CREATE TABLE categorias (
    id        INTEGER PRIMARY KEY,
    nome      TEXT NOT NULL,
    descricao TEXT
);
.restore de um arquivo que não existe: o banco some
$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> .restore nao-existe.db
sqlite> .tables
sqlite> SELECT count(*) FROM clientes;
Parse error: no such table: clientes

O que aconteceu: este é o erro mais perigoso da aula, porque não aparece mensagem nenhuma. O .restore tentou abrir nao-existe.db; como o arquivo não existia, o SQLite criou um banco novo, vazio, com esse nome, e restaurou esse vazio por cima do loja.db. O .tables não mostra nada e "não existe a tabela clientes". Se a Ana não tivesse outra cópia, a loja teria perdido tudo.

Como consertar: aqui havia o copia.db, então deu para voltar:

sqlite> .restore copia.db
sqlite> SELECT count(*) FROM clientes;
30
sqlite> .quit
$ ls -l loja.db nao-existe.db
-rw-r--r-- 1 ana ana 45056 out 18 11:19 loja.db
-rw-r--r-- 1 ana ana     0 out 18 11:19 nao-existe.db
$ rm nao-existe.db

Repare no nao-existe.db com 0 bytes, que o .restore deixou para trás. Para nunca cair nessa: antes de um .restore, confira o nome com ls e faça um .backup do banco atual (por exemplo, .backup antes-do-restore.db). Assim, até um .restore errado tem volta.

VACUUM INTO num arquivo que já existe
$ sqlite3 loja.db "VACUUM INTO 'compacta.db';"
Error: stepping, output file already exists

O que aconteceu: "o arquivo de saída já existe" (o stepping diz que o erro veio ao executar o comando). Ao contrário do .backup, o VACUUM INTO se recusa a apagar um arquivo, e isso é uma proteção. Dentro do sqlite3, a mesma situação aparece como Runtime error: output file already exists.

Como consertar: use outro nome (a data no nome resolve) ou apague o arquivo antigo antes, se tiver certeza.

Backup numa pasta que não existe
sqlite> .backup pasta-que-nao-existe/copia.db
Error: cannot open "pasta-que-nao-existe/copia.db"

O que aconteceu: "não consigo abrir". O sqlite3 cria o arquivo, mas não cria pastas.

Como consertar: crie a pasta antes, no terminal: mkdir -p ~/backups, e depois .backup ~/backups/copia.db.

Abriu o dump como se fosse um banco
$ sqlite3 backup.sql ".tables"
Error: file is not a database

O que aconteceu: "o arquivo não é um banco de dados". O backup.sql é texto, a receita; o sqlite3 esperava um arquivo de banco, o bolo pronto.

Como consertar: um dump se roda num banco novo: sqlite3 novo.db < backup.sql. Só o que o .backup e o VACUUM INTO geram se abre direto.


Nos outros bancos

Backup no MySQL, PostgreSQL e SQL Server

A ideia da "receita" existe em todos: um programa que escreve o banco como SQL. Os trechos são ilustrativos (não rodam no SQLite), conforme a documentação oficial de cada banco:

mysqldump loja > backup.sql      # MySQL: o "dump" do banco loja
mysql loja < backup.sql          # MySQL: restaurar (o banco loja precisa existir)

pg_dump loja > backup.sql        # PostgreSQL: o "dump" em SQL
psql -X loja < backup.sql        # PostgreSQL: restaurar num banco já criado
-- SQL Server: o backup é um comando SQL, gravado pelo servidor
BACKUP DATABASE loja TO DISK = 'C:\backups\loja.bak';
RESTORE DATABASE loja FROM DISK = 'C:\backups\loja.bak';

Nos bancos com servidor, o banco não é um arquivo que você possa simplesmente copiar: são vários arquivos, em uso o tempo todo pelo servidor. Por isso as ferramentas próprias são o único jeito seguro.


Referência rápida

ComandoO que faz
.dumpEscreve o banco inteiro como SQL
.dump tabelaEscreve só uma tabela
sqlite3 loja.db .dump > backup.sqlSalva o dump num arquivo
sqlite3 novo.db < backup.sqlRecria o banco a partir do dump
.read backup.sqlRoda um arquivo de SQL de dentro do sqlite3
.backup copia.dbCópia exata e consistente do banco aberto (substitui o destino)
.restore copia.dbSubstitui o banco aberto pela cópia
VACUUM INTO 'copia.db';Cópia compactada, em SQL (não substitui arquivo existente)
PRAGMA integrity_check;Confere se o banco (ou a cópia) está saudável: responde ok
MensagemQuer dizer
Parse error near line 3: table categorias already existsDump restaurado num banco que já tem as tabelas
Runtime error near line 8: UNIQUE constraint failed: categorias.id (19)A linha já existe no banco
Error: stepping, output file already existsVACUUM INTO num arquivo que já existe
Error: cannot open "…"Pasta ou arquivo inexistente
Error: file is not a databaseTentou abrir um arquivo de texto (um dump) como banco
(nenhuma mensagem) depois de .restoreConfira com .tables: um nome errado restaura um banco vazio

Exercícios

1. O dump num arquivo

Complete a linha do terminal que salva o banco inteiro como SQL no arquivo backup.sql:

sqlite3 loja.db . backup.sql

2. A cópia exata

Dentro do sqlite3, complete o comando que grava uma cópia do banco aberto em copia.db, e o que traz essa cópia de volta:

. copia.db
. copia.db

3. O que tem dentro do arquivo gerado por sqlite3 loja.db .dump > backup.sql?

4. A Ana digitou .restore copai.db (com o nome errado). O que acontece?

5. Por que o .backup é melhor que o cp loja.db copia.db?

6. O sqlite3 loja.db < backup.sql respondeu table categorias already exists. O que a Ana deveria ter feito?

Missão final: a tabela que sumiu

Missão: alguém apagou a tabela categorias inteira. Por sorte, a Ana tem o .dump categorias da seção 1. Cole o dump no lugar indicado (do PRAGMA ao COMMIT) e rode: a contagem do fim tem de dar 8.

PRAGMA foreign_keys = OFF;
DROP TABLE categorias;   -- o acidente

-- cole aqui o dump da tabela categorias

SELECT count(*) AS categorias FROM categorias;
PRAGMA foreign_keys = OFF;
DROP TABLE categorias;   -- o acidente

PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE categorias (
    id        INTEGER PRIMARY KEY,
    nome      TEXT NOT NULL,
    descricao TEXT
);
INSERT INTO categorias VALUES(1,'Papelaria','Material escolar e de escritório');
INSERT INTO categorias VALUES(2,'Eletrônicos','Acessórios e dispositivos eletrônicos');
INSERT INTO categorias VALUES(3,'Casa e Cozinha','Utensílios para o lar');
INSERT INTO categorias VALUES(4,'Livros','Livros, dicionários e guias');
INSERT INTO categorias VALUES(5,'Brinquedos','Brinquedos e jogos para todas as idades');
INSERT INTO categorias VALUES(6,'Esporte e Lazer','Artigos esportivos e de lazer');
INSERT INTO categorias VALUES(7,'Alimentos','Alimentos não perecíveis e regionais');
INSERT INTO categorias VALUES(8,'Jardim','Ferramentas e artigos de jardinagem');
COMMIT;

SELECT count(*) AS categorias FROM categorias;

Rode primeiro o código como está: o SELECT do fim dá no such table: categorias, porque a tabela foi apagada. Depois de colar o dump, as 8 categorias voltam. Por que o PRAGMA foreign_keys = OFF no começo? Com ele ligado, o SQLite não deixa apagar uma tabela que outras tabelas usam (os produtos apontam para as categorias), e o "acidente" nem aconteceria. Você vai ver esse pragma em PRAGMAs Úteis.


Resumo

  • O banco SQLite é um arquivo só: perder o arquivo é perder tudo. Faça backup e guarde em outro lugar.
  • .dump escreve o banco como SQL (CREATE TABLE + INSERT, numa transação). sqlite3 loja.db .dump > backup.sql salva; sqlite3 novo.db < backup.sql restaura, sempre num banco novo.
  • .backup copia.db grava uma cópia exata e consistente; .restore copia.db troca o banco aberto pela cópia. Os dois substituem o destino sem perguntar.
  • VACUUM INTO 'copia.db' é a cópia em SQL, já compactada, e não sobrescreve arquivo existente.
  • cp só com o banco fechado. Confira os backups com PRAGMA integrity_check.
  • Cuidado com o .restore de um nome errado: ele restaura um banco vazio, sem aviso. Faça um .backup antes de qualquer .restore.