SQL Transações: BEGIN e COMMIT
Registrar uma venda na Loja Aurora não é um comando só. É preciso criar o pedido, anotar cada item e tirar os produtos do estoque. Se a luz acabar entre o segundo e o terceiro passo, o banco fica com um pedido registrado e um estoque que não baixou. Para isso existem as transações: um jeito de dizer ao banco "estes comandos são uma coisa só; ou entram todos, ou não entra nenhum". Nesta aula, você vai abrir uma transação com BEGIN, gravá-la com COMMIT e ver o que outro programa enxerga enquanto ela está aberta.
- O que é uma transação e a ideia do "tudo ou nada".
- Que, sem você pedir, cada comando já é uma transação sozinho (o modo automático).
- A abrir uma transação com
BEGINe gravar tudo de uma vez comCOMMIT. - Que, dentro da transação, você vê as suas mudanças, mas os outros programas não.
- O que é
database is lockede por que ele aparece. - Por que mil
INSERTnuma transação ficam centenas de vezes mais rápidos. - As mensagens reais quando um
BEGINou umCOMMITestá sobrando, e por que um erro no meio não desfaz nada sozinho.
Os exemplos com quadro "Resultado" rodam no SQLite do seu navegador, com a Loja Aurora. Lembre que no editor cada Rodar começa com o banco limpo: a transação inteira (o BEGIN, os comandos e o COMMIT) precisa estar na mesma caixa. As partes com dois programas usando o banco ao mesmo tempo saíram do terminal da Ana, com o arquivo ~/loja.db.
A ideia: o carrinho do supermercado
No supermercado, você vai pondo as compras no carrinho. Enquanto não passa no caixa, nada é seu de verdade: você pode tirar coisas, desistir de tudo e ir embora. Quando paga, a compra inteira acontece de uma vez. Nunca acontece de metade do carrinho ser paga e a outra metade não.
| No supermercado | No banco |
|---|---|
| Pegar o carrinho | BEGIN: começa a transação |
| Pôr as compras no carrinho | Os INSERT, UPDATE e DELETE |
| Olhar o que está no carrinho | Um SELECT dentro da transação: você vê as suas mudanças |
| Pagar no caixa | COMMIT ("confirmar"): tudo é gravado de uma vez |
| Largar o carrinho e ir embora | ROLLBACK ("voltar atrás"): nada é gravado (próxima aula, ROLLBACK e Savepoints) |
Uma transação é esse pacote de comandos que o banco trata como uma coisa só. O nome vem do comércio: uma transação é uma troca completa, que acontece inteira ou não acontece.
1. Você já usa transações sem saber
Desde a aula de UPDATE, cada comando que você rodou já era uma transação: o SQLite abre uma, roda o comando e grava, sozinho. Isso se chama modo automático (em inglês, autocommit, "confirmação automática"). Por isso um UPDATE que mexe em 45 linhas nunca para na 20ª: ou muda as 45, ou nenhuma.
O problema aparece quando a tarefa tem vários comandos. No modo automático, cada um é gravado na hora, separado dos outros. Se o terceiro falhar, os dois primeiros já ficaram no banco. Para juntar vários comandos numa transação só, você mesmo a abre e a fecha.
2. BEGIN e COMMIT
O cliente Paulo Sérgio Andrade (id 27), que nunca tinha comprado na loja, levou 2 lápis (produto 3, R$ 1,15 cada), atendido pelo vendedor Caio Teles (id 2). A venda são três comandos, dentro de uma transação:
BEGIN; INSERT INTO pedidos (id, cliente_id, funcionario_id, data, entregue_em, frete) VALUES (91, 27, 2, '2026-10-18', NULL, 0.0); INSERT INTO itens_pedido (pedido_id, produto_id, quantidade, preco_unitario) VALUES (91, 3, 2, 1.15); UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; COMMIT; SELECT p.id, c.nome AS cliente, i.quantidade, pr.nome AS produto, pr.estoque FROM pedidos AS p JOIN clientes AS c ON c.id = p.cliente_id JOIN itens_pedido AS i ON i.pedido_id = p.id JOIN produtos AS pr ON pr.id = i.produto_id WHERE p.id = 91;
- BEGIN;
- "Comece". Abre a transação. A partir daqui, nada é gravado de verdade até o
COMMIT. (BEGIN TRANSACTION;é a mesma coisa, escrita por extenso.) - INSERT INTO pedidos …
- Cria o pedido 91 (veja INSERT INTO).
- INSERT INTO itens_pedido …
- Anota o item: 2 unidades do produto 3, a 1.15.
- UPDATE produtos …
- Baixa o estoque do lápis de 430 para 428.
- COMMIT;
- "Confirme". Grava os três comandos de uma vez e fecha a transação.
- SELECT …
- Já fora da transação, confere a venda juntando as quatro tabelas (veja INNER JOIN).
No quadro, cada comando aparece com a sua resposta: "BEGIN: ok", "1 linha afetada" para cada INSERT e para o UPDATE, "COMMIT: ok" e, no fim, a tabela com a venda e o estoque já em 428.
Dentro da transação, você vê as suas mudanças
Antes do COMMIT, as mudanças ainda não estão gravadas. Mesmo assim, quem abriu a transação já as enxerga, como as compras no próprio carrinho:
SELECT estoque FROM produtos WHERE id = 3; BEGIN; UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; SELECT estoque FROM produtos WHERE id = 3; COMMIT; SELECT estoque FROM produtos WHERE id = 3;
O primeiro SELECT mostra 430. O segundo, ainda dentro da transação, já mostra 428: você vê o próprio UPDATE. O terceiro, depois do COMMIT, mostra 428 gravado. A diferença entre o segundo e o terceiro só aparece para outros programas, e é isso que a seção 4 mostra.
Experimente: registre outra venda numa transação. A cliente Tainá Soares (id 28) comprou 1 Mochila Escolar Azul (produto 5, R$ 89,99), atendida pela vendedora Renata Borges (id 3), em '2026-10-18', com frete de 15.0. O pedido já está escrito; falta o item e a baixa no estoque (que é de 35).
BEGIN; INSERT INTO pedidos (id, cliente_id, funcionario_id, data, entregue_em, frete) VALUES (91, 28, 3, '2026-10-18', NULL, 15.0); -- falta o item do pedido 91 e a baixa no estoque do produto 5 COMMIT; SELECT nome, estoque FROM produtos WHERE id = 5;
BEGIN; INSERT INTO pedidos (id, cliente_id, funcionario_id, data, entregue_em, frete) VALUES (91, 28, 3, '2026-10-18', NULL, 15.0); INSERT INTO itens_pedido (pedido_id, produto_id, quantidade, preco_unitario) VALUES (91, 5, 1, 89.99); UPDATE produtos SET estoque = estoque - 1 WHERE id = 5; COMMIT; SELECT nome, estoque FROM produtos WHERE id = 5;
3. Sem COMMIT, nada fica
No terminal, o banco é um arquivo que continua existindo depois que você sai. Veja o que acontece se a Ana abre uma transação, muda o estoque e sai do sqlite3 sem o COMMIT:
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> SELECT estoque FROM produtos WHERE id = 3; 430 sqlite> BEGIN; sqlite> UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; sqlite> SELECT estoque FROM produtos WHERE id = 3; 428 sqlite> .quit $ sqlite3 loja.db "SELECT estoque FROM produtos WHERE id = 3;" 430 « a mudança sumiu: faltou o COMMIT
Dentro da transação, o estoque já era 428. Mas a transação nunca foi confirmada, e uma transação que termina sem COMMIT (porque o programa fechou, travou ou a luz acabou) é desfeita pelo SQLite. Na próxima abertura, o estoque está em 430, como se nada tivesse acontecido. É exatamente o "tudo ou nada": a transação não terminou, então não aconteceu.
4. O que os outros programas enxergam
Um banco costuma ser usado por mais de um programa ao mesmo tempo: o caixa da loja registra vendas enquanto o sistema de estoque consulta os produtos. Para ver isso, a Ana abriu duas janelas de terminal, cada uma com o seu sqlite3, no mesmo loja.db. Na primeira, ela começa uma venda e não confirma ainda:
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> BEGIN; sqlite> UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; sqlite> SELECT estoque FROM produtos WHERE id = 3; 428
Com a transação do caixa ainda aberta, ela vai para a segunda janela e consulta o mesmo produto. Depois, tenta mudar o estoque:
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> SELECT estoque FROM produtos WHERE id = 3; 430 sqlite> UPDATE produtos SET estoque = 0 WHERE id = 3; Runtime error: database is locked (5)
Duas coisas aconteceram:
- A segunda janela vê o estoque antigo, 430. A mudança do caixa ainda está "no carrinho": ninguém mais a enxerga até o
COMMIT. Assim, ninguém toma decisões com dados que talvez nunca sejam gravados. - A segunda janela não consegue escrever: "erro ao executar: o banco está travado" (o 5 é o código de erro do SQLite para "travado"). No SQLite, só uma transação por vez pode escrever no banco. Enquanto a do caixa estiver aberta com mudanças, as outras esperam ou desistem.
Então o caixa confirma a venda:
sqlite> COMMIT;
E, na segunda janela, a mesma consulta agora mostra o valor novo:
sqlite> SELECT estoque FROM produtos WHERE id = 3; 428
| Momento | Terminal 1 (caixa) | Terminal 2 (estoque) |
|---|---|---|
| 1 | BEGIN; e o UPDATE: vê 428 | |
| 2 | (transação aberta) | SELECT: vê 430, o valor gravado |
| 3 | (transação aberta) | UPDATE: database is locked |
| 4 | COMMIT; | |
| 5 | SELECT: vê 428 |
Uma transação esquecida aberta (alguém deu BEGIN, mudou uma linha e foi almoçar) trava a escrita de todos os outros programas. Abra a transação o mais tarde possível, faça o que precisa e dê o COMMIT logo. Nunca deixe uma transação esperando alguém digitar alguma coisa.
Se a espera for normal (um programa que só precisa aguardar alguns instantes), o sqlite3 pode ser mandado a tentar de novo por um tempo antes de desistir, com .timeout 5000 (5000 milissegundos = 5 segundos). Programas em Python, PHP e outras linguagens têm uma opção parecida, o busy timeout ("tempo de espera quando ocupado").
5. Transação deixa tudo mais rápido
Gravar de verdade no disco é a parte lenta de um banco: a cada COMMIT, o SQLite espera o disco garantir que os dados estão lá. No modo automático, isso acontece a cada comando. Numa transação, uma vez só, no fim.
A Ana montou um arquivo com mil INSERT numa tabela de teste, visitas, e uma segunda versão do mesmo arquivo com BEGIN; na primeira linha e COMMIT; na última. Depois, mediu o tempo de cada um com o time do terminal, em dois bancos novos:
$ head -2 visitas-transacao.sql BEGIN; INSERT INTO visitas VALUES (1, 'pagina-1'); $ tail -1 visitas-transacao.sql COMMIT; $ time sqlite3 sem.db < visitas.sql real 0m9,944s « mil INSERTs, cada um gravado sozinho user 0m0,041s sys 0m0,234s $ time sqlite3 com.db < visitas-transacao.sql real 0m0,016s « os mesmos mil, numa transação só user 0m0,000s sys 0m0,004s $ sqlite3 sem.db "SELECT count(*) FROM visitas;" 1000 $ sqlite3 com.db "SELECT count(*) FROM visitas;" 1000
Os dois bancos terminaram com as mesmas 1000 linhas. Sem transação, levou quase 10 segundos; com transação, 16 milésimos de segundo, umas seiscentas vezes mais rápido. O número exato muda de computador para computador (depende do disco), mas a diferença é sempre enorme. Foi por isso que o .dump da aula Backup veio com um BEGIN TRANSACTION no começo e um COMMIT no fim.
6. Formas diferentes de escrever
O SQLite aceita vários jeitos de abrir e fechar uma transação. Todos estes rodam:
BEGIN TRANSACTION; UPDATE produtos SET descontinuado = 1 WHERE id = 43; END TRANSACTION; BEGIN IMMEDIATE; UPDATE produtos SET descontinuado = 1 WHERE id = 44; COMMIT TRANSACTION; SELECT id, nome, descontinuado FROM produtos WHERE id IN (43, 44);
| Escrita | O que faz | Quando usar |
|---|---|---|
BEGIN; ou BEGIN TRANSACTION; | Abre a transação. Só pede a vez de escrever no primeiro comando que muda alguma coisa | O normal |
BEGIN DEFERRED; | O mesmo que BEGIN (deferred = "adiado"); é o padrão | Raramente escrito por extenso |
BEGIN IMMEDIATE; | Pede a vez de escrever já no BEGIN (immediate = "imediato"). Se outro programa estiver escrevendo, o erro database is locked vem logo no começo, e não no meio da tarefa | Quando você sabe que vai escrever: o erro aparece antes de você ter feito qualquer coisa |
BEGIN EXCLUSIVE; | Parecido com o IMMEDIATE, ainda mais restrito (exclusive = "exclusivo") | Casos especiais |
COMMIT;, COMMIT TRANSACTION;, END;, END TRANSACTION; | Todos confirmam e fecham a transação | Prefira COMMIT: é o que todo banco entende |
Quando dá errado
COMMIT sem BEGIN
UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; COMMIT;
O que aconteceu: "não posso confirmar: nenhuma transação está ativa". O UPDATE rodou no modo automático e já foi gravado sozinho; o COMMIT não tinha nada para confirmar. É um erro inofensivo, mas mostra que o BEGIN ficou faltando, e com ele a proteção do "tudo ou nada". No terminal, a mensagem vem com o prefixo: Runtime error: cannot commit - no transaction is active.
Como consertar: comece com BEGIN; tudo o que precisa ir junto.
BEGIN dentro de BEGIN
BEGIN; UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; BEGIN;
O que aconteceu: "não posso começar uma transação dentro de uma transação". No SQLite, as transações não se encaixam uma dentro da outra. Atenção: a primeira transação continua aberta, com o UPDATE pendente. Para "transações dentro de transações", existem os savepoints, da próxima aula.
Como consertar: feche a transação atual com COMMIT antes de abrir outra.
Este é o engano mais comum sobre transações. A Ana começa uma venda no terminal, baixa o estoque e esquece o preço do item:
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> BEGIN; sqlite> UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; sqlite> INSERT INTO itens_pedido (pedido_id, produto_id, quantidade) VALUES (90, 3, 2); Runtime error: NOT NULL constraint failed: itens_pedido.preco_unitario (19) sqlite> SELECT estoque FROM produtos WHERE id = 3; 428 sqlite> COMMIT; sqlite> .quit $ sqlite3 loja.db "SELECT estoque FROM produtos WHERE id = 3;" 428
O que aconteceu: o INSERT falhou ("a restrição NOT NULL falhou em itens_pedido.preco_unitario": faltou o preço, veja NOT NULL). Mas o SQLite desfez só aquele comando: a transação continuou aberta, com o UPDATE do estoque valendo. Quando a Ana deu COMMIT, gravou o estoque baixado sem o item da venda. Exatamente o que a transação devia evitar.
Como consertar: quando um comando da transação falhar, não dê COMMIT: desfaça tudo com ROLLBACK e comece de novo. É o assunto da próxima aula, ROLLBACK e Savepoints.
No editor, o mesmo erro para a execução ali, e o banco do editor é jogado fora no fim de cada Rodar. Por isso, no editor, nada do que veio antes do erro "fica"; no terminal, com um arquivo de verdade, fica:
BEGIN; UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; INSERT INTO itens_pedido (pedido_id, produto_id, quantidade) VALUES (90, 3, 2); COMMIT;
database is locked
sqlite> UPDATE produtos SET estoque = 0 WHERE id = 3; Runtime error: database is locked (5)
O que aconteceu: outro programa (ou outra janela do terminal) está com uma transação de escrita aberta no mesmo banco, como na seção 4.
Como consertar: descubra quem está com a transação aberta e faça-o terminar (COMMIT ou ROLLBACK). Se for só uma espera curta e normal, use .timeout 5000 no sqlite3 (ou o busy timeout do seu programa). E mantenha as suas transações curtas.
Nos outros bancos
Os três também trabalham no modo automático por padrão: sem você abrir uma transação, cada comando é confirmado sozinho. O jeito de abrir muda um pouco (trechos ilustrativos, conforme a documentação oficial):
-- SQL padrão; MySQL e PostgreSQL também aceitam BEGIN START TRANSACTION; UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; COMMIT; -- SQL Server BEGIN TRANSACTION; -- ou BEGIN TRAN UPDATE produtos SET estoque = estoque - 2 WHERE id = 3; COMMIT TRANSACTION;
Uma diferença importante: no PostgreSQL, um erro dentro da transação a deixa "abortada", e todo comando seguinte é recusado até você encerrá-la; nem dá para dar o COMMIT do estoque sem o item, como a Ana fez acima. No SQLite, quem decide o que fazer depois do erro é você. E nos bancos com servidor, em geral, em vez de travar o banco inteiro para escrever, cada transação trava só as linhas que mudou, então duas vendas de produtos diferentes podem acontecer ao mesmo tempo.
Referência rápida
| Comando | O que faz |
|---|---|
BEGIN; / BEGIN TRANSACTION; | Abre uma transação |
BEGIN IMMEDIATE; | Abre e já reserva a escrita |
COMMIT; / END; | Grava tudo e fecha a transação |
ROLLBACK; | Desfaz tudo e fecha a transação (próxima aula) |
(sem BEGIN) | Modo automático: cada comando é uma transação |
.timeout 5000 | No sqlite3: espera até 5 s antes de dar database is locked |
| Mensagem | Quer dizer |
|---|---|
cannot commit - no transaction is active | COMMIT (ou END) sem transação aberta |
cannot start a transaction within a transaction | BEGIN com outra transação já aberta |
database is locked (5) | Outro programa está escrevendo no banco |
Exercícios
1. Abrir e fechar
Complete para que as duas mudanças sejam gravadas juntas:
UPDATE produtos SET estoque = estoque - 1 WHERE id = 1;
UPDATE produtos SET estoque = estoque + 1 WHERE id = 2;
;
2. Sem nenhum BEGIN, a Ana roda um UPDATE. O que acontece?
3. No terminal, a Ana deu BEGIN, apagou um cliente e fechou o sqlite3 sem COMMIT. Ao abrir de novo, o cliente…
4. Com uma transação aberta no Terminal 1, que mudou o estoque de 430 para 428, o Terminal 2 consulta o estoque. O que ele vê?
5. Dentro de uma transação, o segundo de três comandos deu erro. No SQLite, o que acontece com o primeiro?
Missão final: a venda completa
Missão: registre numa transação só a venda do pedido 91: o cliente Paulo Sérgio Andrade (id 27) levou 3 Cadernos Pautados (produto 1, a 12.35) e 2 Canetas Azuis (produto 2, a 2.79), atendido por Caio Teles (id 2), em '2026-10-18', com frete 9.9. São cinco comandos entre o BEGIN e o COMMIT: o pedido, os dois itens e as duas baixas de estoque (o caderno tem 120, a caneta, 500).
BEGIN; -- 1. o pedido 91 -- 2. os dois itens -- 3. as duas baixas de estoque COMMIT; SELECT nome, estoque FROM produtos WHERE id IN (1, 2);
BEGIN;
INSERT INTO pedidos (id, cliente_id, funcionario_id, data, entregue_em, frete)
VALUES (91, 27, 2, '2026-10-18', NULL, 9.9);
INSERT INTO itens_pedido (pedido_id, produto_id, quantidade, preco_unitario)
VALUES (91, 1, 3, 12.35),
(91, 2, 2, 2.79);
UPDATE produtos SET estoque = estoque - 3 WHERE id = 1;
UPDATE produtos SET estoque = estoque - 2 WHERE id = 2;
COMMIT;
SELECT nome, estoque FROM produtos WHERE id IN (1, 2);
Os dois itens podem ir num INSERT só, com duas linhas de valores (veja Inserindo Várias Linhas), ou em dois INSERT separados: dentro da transação, dá no mesmo. O que importa é que, se qualquer um dos cinco falhasse, a Ana poderia desfazer tudo, e nenhum programa veria uma venda pela metade.
Resumo
- Uma transação é um pacote de comandos que o banco trata como uma coisa só: tudo ou nada.
- Sem
BEGIN, cada comando é uma transação sozinho e é gravado na hora (modo automático, autocommit). BEGIN;abre a transação;COMMIT;grava tudo de uma vez. SemCOMMIT(programa fechado, luz que acaba), a transação é desfeita.- Dentro da transação você vê as suas mudanças; os outros programas só as veem depois do
COMMIT. Enquanto ela escreve, os outros recebemdatabase is locked: mantenha as transações curtas. - Muitos
INSERTnuma transação ficam centenas de vezes mais rápidos: o disco só é esperado uma vez. - No SQLite, um erro no meio desfaz só o comando que falhou; a transação continua aberta. Não dê
COMMITdepois de um erro: useROLLBACK.