SQL As Garantias ACID
Por que uma loja confia o caixa dela a um banco de dados, e não a uma planilha? Porque um banco de dados de verdade faz quatro promessas sobre cada transação, e cumpre mesmo quando a luz acaba, o programa trava ou duas pessoas mexem no mesmo dado ao mesmo tempo. Essas promessas têm um apelido famoso, formado pelas iniciais em inglês: ACID. Nesta aula, a Ana põe cada uma à prova no SQLite: ela vai matar um programa no meio de uma transação, tentar quebrar as regras do banco e abrir duas janelas ao mesmo tempo.
- O que quer dizer cada letra: Atomicidade, Consistência, Isolamento e Durabilidade.
- Que até um único
UPDATEé "tudo ou nada", e o que acontece quando o programa morre no meio de uma transação. - Que as regras da tabela (
NOT NULL,CHECK, chaves estrangeiras) são o que mantém o banco consistente, e que no terminal uma delas vem desligada. - O que uma transação enxerga das outras, e a "foto" que o modo WAL tira do banco.
- O que o
COMMITgarante, e por que o banco do editor do guia não é durável.
Esta aula junta as duas anteriores, Transações e ROLLBACK e Savepoints. Os exemplos com quadro "Resultado" rodam no SQLite do seu navegador. Os testes que dependem de um arquivo de verdade, de dois programas ao mesmo tempo ou de "matar" um programa saíram do terminal da Ana, com o banco ~/loja.db.
A ideia: a transferência entre o caixa e o cofre
No fim do dia, a Ana tira R$ 200 do caixa da loja e guarda no cofre. São dois passos: tirar do caixa e pôr no cofre. Agora imagine as coisas que podem dar errado, e o que a loja espera de quem cuida do dinheiro:
| Letra | Nome | A promessa | Na transferência |
|---|---|---|---|
| A | Atomicidade | Tudo ou nada | O dinheiro nunca sai do caixa sem chegar ao cofre, nem se a Ana for interrompida no meio |
| C | Consistência | As regras sempre valem | Nenhum dos dois fica com saldo negativo; o total continua o mesmo |
| I | Isolamento | Uma transação não vê a outra pela metade | Quem conta o dinheiro nunca pega o momento em que os R$ 200 estão "na mão da Ana", fora dos dois |
| D | Durabilidade | O que foi confirmado, fica | Depois de anotado no livro do cofre, nem um apagão faz a transferência "desacontecer" |
"Atômico" vem do grego átomos, "indivisível": algo que não se parte em pedaços. As quatro palavras parecem difíceis, mas você já viu duas delas funcionando nas aulas anteriores. Agora, uma por uma.
1. A de Atomicidade: tudo ou nada
Até um único comando é atômico
Para testar, a Ana cria uma cópia do estoque numa tabela com uma regra: a quantidade nunca pode ser negativa (CHECK (qtd >= 0), veja CHECK). Depois, tenta tirar 30 unidades de todos os produtos de uma vez:
CREATE TABLE estoque_teste (id INTEGER PRIMARY KEY, nome TEXT,
qtd INTEGER CHECK (qtd >= 0));
INSERT INTO estoque_teste SELECT id, nome, estoque FROM produtos;
UPDATE estoque_teste SET qtd = qtd - 30;
O caderno tem 120 e a caneta 500: para eles, tirar 30 daria certo. Mas o pen drive tem 0, e 0 − 30 = −30 quebra a regra. O que aconteceu com o caderno e a caneta? O editor para no erro, então a Ana confere no terminal, com o mesmo comando:
sqlite> UPDATE estoque_teste SET qtd = qtd - 30; Runtime error: CHECK constraint failed: qtd >= 0 (19) sqlite> SELECT id, nome, qtd FROM estoque_teste WHERE id IN (1, 2, 12); 1|Caderno Pautado 96 folhas|120 2|Caneta Esferográfica Azul|500 12|Pen Drive 32GB|0
Nada mudou: nem o caderno, nem a caneta. O UPDATE é um comando, e um comando é atômico: como uma das linhas falhou, o SQLite desfez as que já tinha mudado. Nunca acontece de "metade da tabela" ficar atualizada. Para vários comandos virarem uma coisa só, você já sabe: BEGIN … COMMIT.
E se o programa morrer no meio da transação?
Este é o teste de verdade. A Ana manda um sqlite3 apagar todos os itens de pedido dentro de uma transação e deixa o programa parado ali, sem COMMIT, rodando em segundo plano (o & no fim). Depois, ela o mata com kill -9, que encerra o programa na hora, sem dar chance de ele arrumar nada; é o mais perto que dá de puxar o cabo da tomada:
$ ( echo "BEGIN;"; echo "DELETE FROM itens_pedido;"; sleep 30 ) | sqlite3 loja.db & [1] 4 $ sleep 1 $ ls -l loja.db* -rw-r--r-- 1 ana ana 45056 out 18 11:14 loja.db -rw-r--r-- 1 ana ana 21032 out 18 11:19 loja.db-journal $ kill -9 $! $ sqlite3 loja.db "SELECT count(*) FROM itens_pedido;" 220 $ sqlite3 loja.db "PRAGMA integrity_check;" ok
- ( echo …; sleep 30 ) | sqlite3 loja.db &
- Entrega ao
sqlite3oBEGINe oDELETEe, com osleep 30, segura a transação aberta por 30 segundos. O&roda tudo em segundo plano, e o terminal fica livre. - loja.db-journal
- O diário (journal) da transação. Antes de mudar qualquer pedaço do banco, o SQLite guarda ali uma cópia de como esse pedaço era. Repare que o
loja.dbcontinua com a hora antiga (11:14): o banco em si nem foi tocado ainda. - kill -9 $!
- Mata o
sqlite3(o$!é o último programa posto em segundo plano) no meio da transação. - 220
- Os 220 itens continuam lá. A transação não terminou, então não aconteceu.
- ok
- O banco está saudável (aula PRAGMAs Úteis).
É o diário que garante a atomicidade mesmo num desastre. Se o programa morre depois de já ter gravado parte das mudanças no banco, o próximo programa que abrir o arquivo encontra o diário, percebe que ficou uma transação pela metade e usa as cópias guardadas para pôr tudo de volta como era, antes de deixar qualquer um ler.
-journal (nem o -wal)
Depois de um travamento, o arquivo loja.db-journal (ou, no modo WAL, o loja.db-wal) pode ficar na pasta. Parece lixo, mas pode ser justamente o que o SQLite precisa para desfazer uma transação pela metade. Apagá-lo à mão é um dos poucos jeitos de estragar um banco SQLite. Deixe que o próprio SQLite cuide dele: na próxima vez que alguém abrir e escrever no banco, ele some sozinho.
2. C de Consistência: as regras sempre valem
Consistência quer dizer que o banco passa de um estado válido para outro estado válido. "Válido" é o que as regras da tabela dizem: NOT NULL, UNIQUE, CHECK, chaves primárias e estrangeiras (aulas do grupo de CREATE TABLE). Um comando que quebraria uma regra é recusado inteiro.
Um pedido para um cliente que não existe (não há cliente 99) quebraria a regra da chave estrangeira pedidos.cliente_id → clientes.id (veja FOREIGN KEY):
INSERT INTO pedidos (id, cliente_id, funcionario_id, data, frete) VALUES (91, 99, 2, '2026-10-18', 0);
"A restrição de chave estrangeira falhou": o banco se recusou a ter um pedido sem dono. Até aqui, tudo certo. Mas veja o mesmo INSERT no terminal da Ana:
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> INSERT INTO pedidos (id, cliente_id, funcionario_id, data, frete) VALUES (91, 99, 2, '2026-10-18', 0); sqlite> SELECT id, cliente_id FROM pedidos WHERE id = 91; 91|99 sqlite> SELECT nome FROM clientes WHERE id = 99; sqlite> DELETE FROM pedidos WHERE id = 91; sqlite> PRAGMA foreign_keys; 0 sqlite> PRAGMA foreign_keys = ON; sqlite> INSERT INTO pedidos (id, cliente_id, funcionario_id, data, frete) VALUES (91, 99, 2, '2026-10-18', 0); Runtime error: FOREIGN KEY constraint failed (19)
No terminal, o pedido do cliente 99 entrou, e o cliente 99 não existe. O motivo está no PRAGMA foreign_keys, que respondeu 0: no SQLite, a conferência de chaves estrangeiras vem desligada por padrão, a cada vez que um programa abre o banco (é uma decisão antiga, mantida para não quebrar programas antigos). O editor do guia a liga ao carregar a Loja Aurora; no terminal, a Ana precisou ligar com PRAGMA foreign_keys = ON. A lição: o banco só é tão consistente quanto as regras que estão valendo.
A consistência também vale para a transação inteira. Na transferência do caixa para o cofre, a regra é: nenhum saldo negativo.
Experimente: a transferência de R$ 200 do caixa para o cofre está pela metade: o dinheiro sai do caixa, mas não entra no cofre, e o total da loja "encolhe" de 1250 para 1050. Complete a transação para que o total continue 1250. Depois, para ver a regra funcionando, tente transferir 300 (o caixa só tem 250).
CREATE TABLE caixas (nome TEXT PRIMARY KEY,
saldo REAL NOT NULL CHECK (saldo >= 0));
INSERT INTO caixas VALUES ('caixa', 250.0), ('cofre', 1000.0);
BEGIN;
UPDATE caixas SET saldo = saldo - 200 WHERE nome = 'caixa';
COMMIT;
SELECT nome, saldo FROM caixas;
SELECT sum(saldo) AS total FROM caixas;
CREATE TABLE caixas (nome TEXT PRIMARY KEY,
saldo REAL NOT NULL CHECK (saldo >= 0));
INSERT INTO caixas VALUES ('caixa', 250.0), ('cofre', 1000.0);
BEGIN;
UPDATE caixas SET saldo = saldo - 200 WHERE nome = 'caixa';
UPDATE caixas SET saldo = saldo + 200 WHERE nome = 'cofre';
COMMIT;
SELECT nome, saldo FROM caixas;
SELECT sum(saldo) AS total FROM caixas;
Com 300, o primeiro UPDATE daria saldo −50 no caixa, e o SQLite responde CHECK constraint failed: saldo >= 0. Repare na divisão de trabalho: o banco garante as regras que você escreveu (saldo não negativo). A regra "o total não muda" não está escrita em lugar nenhum; quem a garante é você, pondo os dois UPDATE na mesma transação.
3. I de Isolamento: uma transação não vê a outra pela metade
Na aula Transações, você viu a primeira parte do isolamento: enquanto o caixa não dá COMMIT, a outra janela continua vendo o estoque antigo, e quem tenta escrever ao mesmo tempo recebe database is locked. O SQLite deixa só uma transação escrever por vez, e por isso ninguém nunca enxerga uma mudança pela metade.
Agora a outra parte: e se for quem lê que está numa transação longa? Imagine a contadora somando o estoque, produto por produto, enquanto o caixa continua vendendo. Ela precisa de números que batam entre si, de um mesmo instante. Para ver isso, a Ana liga o modo WAL (write-ahead log, "registro antes da escrita"; mais na aula PRAGMAs Úteis), que deixa quem lê e quem escreve trabalharem ao mesmo tempo. E abre duas janelas de novo.
Na janela da contadora, ela abre uma transação e lê o estoque do lápis:
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> BEGIN; sqlite> SELECT estoque FROM produtos WHERE id = 3; 430
Enquanto isso, na janela do caixa, uma venda de 2 lápis é gravada (sem BEGIN: o UPDATE já é confirmado sozinho):
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> PRAGMA journal_mode = WAL; wal sqlite> UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; sqlite> SELECT estoque FROM produtos WHERE id = 3; 428
A venda foi gravada: o caixa vê 428, e desta vez não houve database is locked. De volta à contadora, que ainda está na transação dela:
sqlite> SELECT estoque FROM produtos WHERE id = 3; 430 sqlite> COMMIT; sqlite> SELECT estoque FROM produtos WHERE id = 3; 428
Mesmo com a venda já gravada, a contadora continua vendo 430 até terminar a transação dela. É como se, no BEGIN, ela tivesse tirado uma foto do banco: tudo o que ela ler na transação vem da mesma foto, e as contas batem. Depois do COMMIT, a próxima leitura já mostra o banco atual: 428.
| Situação | Modo normal do SQLite | Modo WAL |
|---|---|---|
| Uma transação escreve, outra lê | Quem lê vê os dados antigos, gravados | Quem lê vê os dados antigos, gravados |
| Duas transações querem escrever | Uma de cada vez: a outra recebe database is locked | Uma de cada vez: a outra recebe database is locked |
| Uma leitura longa e uma escrita | A escrita pode ter de esperar a leitura terminar para confirmar | As duas seguem juntas; quem lê continua vendo a sua "foto" |
No editor do guia não dá para testar o isolamento: lá só existe um programa usando o banco, o seu navegador.
4. D de Durabilidade: o que foi confirmado, fica
Durabilidade é a promessa do COMMIT: quando ele responde, os dados já estão no disco, e nenhum travamento depois disso os desfaz. A Ana repete o teste do kill -9, agora com uma transação que termina com COMMIT (apagando só os 2 itens do pedido 90) antes de o programa ser morto:
$ ( echo "BEGIN;"; echo "DELETE FROM itens_pedido WHERE pedido_id = 90;"; echo "COMMIT;"; sleep 30 ) | sqlite3 loja.db & [2] 10 $ sleep 1 $ kill -9 $! $ sqlite3 loja.db "SELECT count(*) FROM itens_pedido;" 218 $ ls -l loja.db* -rw-r--r-- 1 ana ana 45056 out 18 11:19 loja.db
O programa morreu do mesmo jeito, mas desta vez depois do COMMIT: os 2 itens continuam apagados (220 − 2 = 218). O loja.db ganhou hora nova (11:19), e o diário sumiu: a transação foi gravada e terminada. Para cumprir essa promessa, a cada COMMIT o SQLite espera o disco confirmar que os dados estão lá de verdade. É essa espera que deixa mil COMMITs separados tão lentos (seção 5 de Transações).
No editor do guia, o banco vive na memória do navegador e é recriado a cada Rodar. É de propósito, para você poder apagar tudo sem medo, mas é o oposto da durabilidade: nada sobrevive ao próximo clique. Num programa de verdade, um banco em memória (sqlite3 sem nome de arquivo, ou :memory:) serve para contas rápidas e testes, nunca para guardar o caixa da loja.
5. ACID numa tabela só
| Garantia | Quer dizer | Quem cuida dela no SQLite | O que você faz |
|---|---|---|---|
| Atomicidade | Tudo ou nada | O diário (-journal) ou o -wal | Põe numa transação o que precisa ir junto |
| Consistência | As regras sempre valem | As restrições: NOT NULL, UNIQUE, CHECK, chaves | Escreve as regras nas tabelas e liga o foreign_keys |
| Isolamento | Ninguém vê uma transação pela metade | As travas (uma escrita por vez) e a "foto" das leituras | Mantém as transações curtas |
| Durabilidade | Confirmado é para sempre | A espera pelo disco a cada COMMIT | Não apaga o -journal/-wal e faz backup (contra disco quebrado, nem ACID salva) |
Uma planilha não faz nenhuma dessas promessas: se o computador desliga enquanto ela salva, o arquivo pode ficar pela metade; duas pessoas editando o mesmo arquivo atrapalham uma à outra; e nada impede alguém de digitar um estoque negativo. É por isso que dados importantes moram num banco.
Quando dá errado
CHECK constraint failed
Na tabela estoque_teste, o UPDATE … SET qtd = qtd - 30 deu CHECK constraint failed: qtd >= 0 e não mudou nenhuma linha.
Como consertar: se a intenção era tirar 30 só de quem tem pelo menos 30, diga isso no WHERE (é a missão final). Não remova a regra: ela está protegendo o banco.
No terminal, o pedido do cliente 99 entrou sem erro nenhum, porque o PRAGMA foreign_keys estava em 0.
Como consertar: rode PRAGMA foreign_keys = ON; logo depois de abrir o banco, em todo programa (ele vale só para aquela conexão). Para achar dados que já entraram quebrados, use PRAGMA foreign_key_check; (aula PRAGMAs Úteis).
-journal depois de um travamento
Se o programa morreu depois de gravar parte da transação no banco, o diário é a única cópia de como as páginas eram. Sem ele, o SQLite não tem como desfazer, e o banco pode ficar com uma transação pela metade, ou estragado.
Como evitar: nunca apague nem mova os arquivos -journal, -wal e -shm que estão ao lado do banco. Para copiar o banco, use o .backup (aula Backup), nunca um cp só do .db.
database is locked
É o isolamento trabalhando: outra transação está escrevendo. Veja a aula Transações para a demonstração com duas janelas e o .timeout.
Nos outros bancos
Os três são bancos ACID, e as ideias desta aula valem para eles. A maior diferença está no isolamento. O SQLite é o mais rígido: as transações acontecem como se fossem uma de cada vez (o nome técnico é isolamento serializable, "serializável"). Os bancos com servidor deixam muitas transações escreverem ao mesmo tempo e oferecem níveis de isolamento, que trocam um pouco de proteção por velocidade:
- PostgreSQL e SQL Server: o padrão é
READ COMMITTED("ler o que foi confirmado"): cada comando vê o que já foi confirmado no momento em que começa. Duas leituras na mesma transação podem dar resultados diferentes, se alguém confirmar algo no meio. - MySQL (com o motor InnoDB, o padrão): o padrão é
REPEATABLE READ("leitura repetível"): parecido com a "foto" do WAL que a contadora viu.
-- ilustrativo: pedir o nível mais rígido numa transação (PostgreSQL, MySQL, SQL Server) SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Os níveis de isolamento são assunto para quando você trabalhar com um desses bancos. No SQLite, não há o que escolher.
Referência rápida
| Termo | Quer dizer |
|---|---|
| Atomicidade | A transação acontece inteira ou não acontece |
| Consistência | O banco vai de um estado válido para outro; as restrições sempre valem |
| Isolamento | Transações ao mesmo tempo não veem as mudanças umas das outras pela metade |
| Durabilidade | Depois do COMMIT, os dados sobrevivem a travamentos e apagões |
loja.db-journal | O diário: cópia do que a transação está mudando, para poder desfazer |
loja.db-wal | No modo WAL: as mudanças novas, antes de irem para o banco |
PRAGMA foreign_keys = ON; | Liga a conferência das chaves estrangeiras (desligada por padrão no terminal) |
PRAGMA journal_mode = WAL; | Liga o modo WAL: leitores e escritor ao mesmo tempo |
Exercícios
1. As quatro letras
Complete a palavra de cada letra:
C =
I =
D =
2. Um UPDATE que mexeria em 45 linhas falhou numa delas por causa de um CHECK. Quantas linhas mudaram?
3. A Ana deu BEGIN, apagou todos os itens e o programa foi morto com kill -9 antes do COMMIT. Que garantia faz os itens continuarem no banco?
4. No terminal, um pedido para um cliente que não existe entrou sem erro. Por quê?
5. No modo WAL, a contadora abriu uma transação e leu 430. O caixa gravou uma venda (428). O que a contadora lê na mesma transação?
6. Depois de um travamento, apareceu um arquivo loja.db-journal na pasta. O que a Ana deve fazer?
Missão final: tirar 30 de quem pode
Missão: o UPDATE abaixo quebra a regra qtd >= 0 e, por ser atômico, não muda nada. Acrescente um WHERE para tirar 30 unidades só dos produtos que têm pelo menos 30. O caderno deve ficar com 90, e o pen drive, que tem 0, continua com 0.
CREATE TABLE estoque_teste (id INTEGER PRIMARY KEY, nome TEXT,
qtd INTEGER CHECK (qtd >= 0));
INSERT INTO estoque_teste SELECT id, nome, estoque FROM produtos;
UPDATE estoque_teste SET qtd = qtd - 30;
SELECT id, nome, qtd FROM estoque_teste WHERE id IN (1, 2, 12);
CREATE TABLE estoque_teste (id INTEGER PRIMARY KEY, nome TEXT,
qtd INTEGER CHECK (qtd >= 0));
INSERT INTO estoque_teste SELECT id, nome, estoque FROM produtos;
UPDATE estoque_teste SET qtd = qtd - 30 WHERE qtd >= 30;
SELECT id, nome, qtd FROM estoque_teste WHERE id IN (1, 2, 12);
Com o WHERE qtd >= 30, o UPDATE mexe em 31 produtos e deixa os outros 14 como estavam. A regra continua lá, protegendo o banco do próximo comando apressado.
Resumo
- ACID são as quatro promessas de um banco de dados para cada transação: Atomicidade, Consistência, Isolamento e Durabilidade.
- Atomicidade: tudo ou nada. Vale para cada comando sozinho e para cada transação, mesmo se o programa morrer no meio (o diário
-journalou o-waldesfaz o que ficou pela metade). - Consistência: as restrições da tabela sempre valem. No terminal do SQLite, ligue as chaves estrangeiras com
PRAGMA foreign_keys = ON. - Isolamento: ninguém vê uma transação pela metade; no SQLite, uma escrita por vez e, no modo WAL, cada leitura numa transação vê a mesma "foto".
- Durabilidade: depois do
COMMIT, os dados estão no disco. O banco em memória do editor não é durável. - Nunca apague os arquivos
-journal/-walao lado do banco, e faça backup: ACID não protege contra um disco quebrado.