Uma transação é um pacote tudo-ou-nada
Uma transação agrupa várias instruções de modo que ou todas surtem efeito, ou nenhuma surte. Se algo der errado no meio do caminho, basta dar rollback e o banco volta exatamente ao estado inicial.
O exemplo clássico é a transferência de dinheiro entre contas:
Os dois UPDATEs formam uma dupla inseparável. Se o banco travasse entre um e outro, Ada ficaria 2000 centavos mais pobre e Boris não receberia nada. Envolver os dois em BEGIN ... COMMIT torna a operação atômica — ou os dois acontecem, ou nenhum acontece.
Autocommit: o padrão que você já está usando no SQLite
Cada comando SQL que você rodou até aqui já foi uma transação. Por padrão, o SQLite opera em modo autocommit: cada instrução ganha seu próprio BEGIN e COMMIT implícitos por trás dos panos.
Três INSERTs, três transações separadas, três idas ao disco para fazer o fsync da alteração. Tudo bem quando é uma gravação isolada, mas vira um gargalo em cargas em massa — e ainda por cima você não consegue desfazer um conjunto de comandos como uma unidade. O BEGIN desliga o autocommit até o próximo COMMIT ou ROLLBACK.
ROLLBACK: fingindo que nada aconteceu
O ROLLBACK descarta tudo o que foi feito desde o BEGIN correspondente. O banco volta ao estado anterior à transação.
Tanto o UPDATE quanto o DELETE somem — a tabela volta a ficar exatamente como estava antes do BEGIN. É essa rede de segurança que permite ao código da aplicação abortar de forma limpa quando algo dá errado no meio de uma operação com vários comandos.
A propósito, uma violação de constraint dentro de uma transação não dispara um rollback automático do bloco inteiro. O SQLite só desfaz o comando que falhou e mantém a transação aberta, esperando você decidir o que fazer. Se quiser comportamento tudo-ou-nada, a aplicação precisa emitir um ROLLBACK ao detectar o erro.
Acelerando inserts em massa com transações
Como cada comando em modo autocommit faz seu próprio fsync, agrupar um lote inteiro dentro de uma única transação costuma ser até 100x mais rápido:
Um único fsync no COMMIT, em vez de um por linha. Se você já tentou importar milhares de linhas e ficou se perguntando por que estava tão lento, pode apostar: é isso.
DEFERRED, IMMEDIATE e EXCLUSIVE
O BEGIN aceita um modo que controla quando o SQLite adquire os locks:
BEGIN DEFERRED(o padrão) — não pega lock nenhum até você ler ou escrever. O lock de escrita é obtido de forma preguiçosa, no primeiro comando que altera dados.BEGIN IMMEDIATE— pega o lock de escrita na hora. As outras conexões ainda conseguem ler, mas nenhuma outra consegue começar a escrever.BEGIN EXCLUSIVE— igual aoIMMEDIATE, só que nenhuma outra conexão consegue nem ler. No modo WAL, esse modo se comporta igual aoIMMEDIATE; a diferença só faz sentido no modo antigo de rollback journal.
BEGIN DEFERRED; -- igual ao BEGIN comum
BEGIN IMMEDIATE; -- reserva o bloqueio de escrita agora
BEGIN EXCLUSIVE; -- reserva tudo (modo rollback-journal)
A escolha importa para concorrência. Com um BEGIN simples, duas conexões podem abrir uma transação ao mesmo tempo, ler tranquilamente e aí dar ruim na hora da escrita — a segunda que pedir o lock de escrita leva um SQLITE_BUSY e, pior ainda, já fez leituras que agora precisam ser descartadas.
O BEGIN IMMEDIATE resolve isso: se você já sabe que vai escrever, peça o lock de escrita logo de cara. A segunda conexão bloqueia (ou falha rapidinho) na hora, antes de fazer qualquer trabalho que teria que jogar fora.
Regra prática: se a transação vai escrever, use BEGIN IMMEDIATE.
Leitura dentro de uma transação enxerga um snapshot
Enquanto uma transação está aberta, suas leituras enxergam um snapshot consistente do banco — do estado em que ele estava quando a transação começou (no modo WAL) ou quando você fez a primeira leitura (no modo rollback-journal). Mudanças commitadas por outras conexões não aparecem do nada nas suas consultas.
Você enxerga suas próprias escritas ainda não confirmadas; outras conexões, não. Assim que você dá COMMIT, o novo valor passa a ser visível para todo mundo. É a isso que as pessoas se referem quando dizem que o SQLite é serializable — não existe botão de READ COMMITTED para girar, porque o padrão já é o nível mais forte de isolamento.
Uma transação no código da aplicação
Num programa de verdade, o padrão costuma ser um try/except (ou try/catch) envolvendo o corpo da operação, com um ROLLBACK no caminho de erro:
-- Pseudocódigo para qualquer biblioteca cliente
BEGIN IMMEDIATE;
try:
UPDATE accounts SET cents = cents - 2000 WHERE owner = 'Ada';
UPDATE accounts SET cents = cents + 2000 WHERE owner = 'Boris';
COMMIT;
except:
ROLLBACK;
raise;
A maioria das bibliotecas cliente (o sqlite3 do Python, o better-sqlite3 e afins) já encapsulam isso com um bloco with ou um helper transaction(). Vale dar uma olhada na documentação da sua lib — os padrões nem sempre são o que você imagina. O sqlite3 do Python, em particular, sempre teve um comportamento meio esquisito de autocommit; versões mais recentes incluíram um parâmetro autocommit decente para resolver isso.
Pegadinhas que costumam pegar todo mundo
- DDL dentro de transação funciona. Dá para fazer rollback de
CREATE TABLE,ALTER TABLEe atéDROP TABLE. O SQLite é incomum nesse aspecto — muitos outros bancos fazem commit automático em DDL. VACUUMnão roda dentro de uma transação. Alguns outros comandos de manutenção também não. Esses precisam rodar em modo autocommit.- Um
COMMITque falha é uma falha de verdade. Se oCOMMITretornarSQLITE_BUSY(raro, mas acontece), a transação não foi confirmada. Seu código precisa tratar isso — normalmente com uma nova tentativa. - Transações longas travam quem quer escrever. Uma transação que fica aberta por minutos vai bloquear outros writers por minutos. Abra tarde, faça commit rápido.
A seguir: savepoints
BEGIN e COMMIT são tudo ou nada. Às vezes você quer desfazer só uma parte da transação — abandonar um passo arriscado, mas manter o resto. É justamente para isso que existem os savepoints, e é o que vem na sequência.
Perguntas frequentes
Como inicio uma transação no SQLite?
Execute BEGIN; (ou BEGIN TRANSACTION;), faça o que precisa fazer e finalize com COMMIT; para salvar ou ROLLBACK; para descartar tudo. Sem um BEGIN explícito, cada comando roda na sua própria transação com commit automático.
Qual a diferença entre BEGIN, BEGIN IMMEDIATE e BEGIN EXCLUSIVE?
BEGIN (equivalente a BEGIN DEFERRED) só pega o lock de escrita quando você realmente escreve — e isso pode estourar lá na frente com SQLITE_BUSY se outra conexão chegou primeiro. BEGIN IMMEDIATE já reserva o lock de escrita logo de cara. Já BEGIN EXCLUSIVE vai além e bloqueia até os leitores (só faz diferença fora do modo WAL).
O SQLite tem níveis de isolamento de transação?
Não no sentido tradicional do SQL. Na prática o SQLite é SERIALIZABLE: a transação enxerga um snapshot consistente e as escritas são serializadas. Não existem botões de READ COMMITTED ou REPEATABLE READ — a escolha que você faz é entre DEFERRED, IMMEDIATE e EXCLUSIVE, e isso controla quando os locks são adquiridos, não o que você consegue enxergar.
O SQLite suporta transações aninhadas?
Diretamente não — você não pode dar BEGIN dentro de outro BEGIN. Para aninhar, use SAVEPOINT junto com RELEASE / ROLLBACK TO, que permitem desfazer parcialmente partes de uma mesma transação. Vou mostrar isso na próxima página.