Dia 10 — SQL: alterar e apagar
UPDATE e ALTER TABLE
UPDATE e ALTER TABLE
UPDATE altera dados. ALTER TABLE altera estrutura. São dois comandos
que nunca aparecem juntos, e a distinção separa "a tabela ficou errada" de "o
dado ficou errado".
UPDATE tb_produto SET qtd = 25 WHERE nm_produto = 'Teclado'; ALTER TABLE tb_produto ADD COLUMN vl_preco DECIMAL(6,2) NULL;
Alterar dados é UPDATE: a linha continua a mesma, e o valor dentro dela muda.
Alterar tabela é o ALTER TABLE, e ele muda a forma da tabela — quantas
colunas ela tem, de que tipo são, e o nome de cada uma. Nenhuma das duas
operações enxerga a outra: um UPDATE não cria coluna e um ALTER TABLE não
muda valor de linha.
SET e WHERE: o WHERE é obrigatório
SET diz o que muda, WHERE diz onde. Sem WHERE, o UPDATE muda a tabela
inteira — e o banco não pergunta se era isso, não avisa e não desfaz.
A defesa não é o WHERE por id: é o WHERE explícito, sempre. `WHERE
qtd = 25 bate em quantas linhas der; WHERE id = 7` bate em uma, e só porque o
id é chave primária.
affectedRows, changedRows e info: três números, três perguntas
| Campo | Pergunta que responde |
|---|---|
affectedRows | quantas linhas o filtro encontrou |
changedRows | quantas mudaram de valor |
info | "Rows matched: 1 Changed: 1 Warnings: 0" |
O mesmo UPDATE executado duas vezes com o mesmo valor dá affectedRows: 1 e
changedRows: 0 na segunda vez. Por isso "o dado mudou" se lê no changedRows ou
no info, e não no affectedRows.
ADD COLUMN: a operação de esquema mais comum
ALTER TABLE t ADD COLUMN vl_preco DECIMAL(6,2) NULL; ALTER TABLE t ADD COLUMN dt_cadastro DATE NOT NULL DEFAULT '2026-05-04'; ALTER TABLE t ADD COLUMN vl_preco DECIMAL(6,2) AFTER nm_produto;
As linhas que já existiam não se perdem: recebem NULL se a coluna é
nullable, ou o DEFAULT se foi declarado. AFTER nome (ou FIRST) só muda a
posição na apresentação.
NOT NULL sem DEFAULT em tabela que já tem linhas falha com
ER_NO_DEFAULT_FOR_FIELD: o banco teria de inventar um valor. Daí a regra — coluna
NOT NULL sempre acompanhada de DEFAULT, ou a alteração em três tempos (add
nullable → backfill → MODIFY para NOT NULL).
MODIFY e CHANGE
| Comando | O que muda |
|---|---|
MODIFY COLUMN qtd SMALLINT NOT NULL | tipo e restrições; o nome fica |
CHANGE COLUMN qtd qtde SMALLINT NOT NULL | tipo e nome, de uma vez |
DROP COLUMN dt_cadastro | destrói a coluna e os valores |
CHANGE tem o nome duas vezes no formato — CHANGE COLUMN antigo novo
tipo — e sem o tipo declarado a coluna perde tudo o que tinha. A mesma
consequência vale para MODIFY: o tipo é reescrito por inteiro, então MODIFY
sem tipo reescreve a coluna como TEXT.
Por que o exemplo recria a tabela em vez de usar IF NOT EXISTS
Renomear coluna é o CHANGE, e o tipo tem de vir junto mesmo quando não muda:
ALTER TABLE tb_produto CHANGE COLUMN qtd qtde SMALLINT NOT NULL DEFAULT 0;
Renomear coluna por CHANGE com o tipo escrito à mão é perigoso numa tabela
grande: se a nova declaração for mais estreita que a coluna antiga, o banco corta
dado sem perguntar. RENAME COLUMN faz só a troca de nome, sem tocar no tipo —
e nem existe em toda versão de servidor, então a prática continua sendo o
CHANGE com o tipo repetido.
Em produção, alterar tabela é migração de esquema: um conjunto de passos
versionado, que leva a estrutura antiga à nova sem passar por um estado que o
código em produção não consiga usar.
| passo | por quê |
|---|---|
| ampliar antes de restringir | ADD COLUMN nullable → backfill → MODIFY NOT NULL |
| coluna nova e código novo na mesma publicação | senão o INSERT antigo quebra em ER_BAD_FIELD_ERROR |
| cada passo sozinho, e reversível por outro passo | ALTER TABLE não tem ROLLBACK |
O DROP COLUMN é o único caminho que destrói dado sem possibilidade de
recuperação, e é por isso que ele vai para o fim da migração, nunca no começo:
enquanto a coluna existe, o código antigo continua funcionando.
O DROP TABLE IF EXISTS no começo deste exemplo não contradiz o desacordo: em
migração, DROP destrói dado que não se pode recuperar. Aqui a tabela é **do
exemplo**, e a recriação é o que garante que a segunda execução comece no mesmo
estado da primeira — o ADD COLUMN de hoje quebraria na rodada seguinte com
ER_DUP_FIELDNAME.
Em migração de verdade, o caminho é IF NOT EXISTS + backfill, e o `SHOW CREATE
TABLE anterior anotado: o ALTER TABLE` não tem como desfazer, e o SQL que
recria a estrutura original precisa estar em algum lugar.
A data volta com fuso
DATE_FORMAT(dt_cadastro, '%Y-%m-%d') devolve o texto do banco, sem fuso. Um
date lido como Date do JavaScript entra com fuso e sai no dia anterior no
toISOString() — porque o servidor está em UTC e a máquina não. Para mostrar
data, formatar no banco.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 10: `UPDATE` e `ALTER TABLE`. // // `UPDATE` altera DADOS; `ALTER TABLE` altera a ESTRUTURA. Sao dois comandos que // nunca aparecem juntos num INSERT, e a distincao e o que separa "a tabela ficou // errada" de "o dado ficou errado". // // A aula 2 de hoje e o outro par: `DELETE` e `TRUNCATE`. E o que o `UPDATE` // mostra aqui, e que vale para os dois: quem apaga sem `WHERE` apaga tudo, e o // banco nao avisa. // // O exemplo monta a tabela, altera a estrutura, e no fim mostra o que a // estrutura era antes — porque `ALTER TABLE` nao tem `undo`, e saber o // `SHOW CREATE TABLE` anterior e o que salva a recuperacao. const mysql = require('mysql2/promise'); async function main() { const conexao = await mysql.createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); try { // --- 1. a tabela no estado inicial --- // // A tabela precisa comecar EXATAMENTE no estado inicial, toda execucao. O // `TRUNCATE` sozinho nao basta: depois da primeira rodada a tabela ja tem // `vl_preco`, `qtde` e `dt_cadastro`, e o `ADD COLUMN` da proxima volta a // falhar com `ER_DUP_FIELDNAME`. Por isso o exemplo apaga e recria — e e // justamente o `DROP TABLE` que a secao 7 abaixo desaconselha em migracao. // Aqui ele e seguro: a tabela e do exemplo, e e recriada no comeco dele. await conexao.query('DROP TABLE IF EXISTS tb_produto'); await conexao.query(` CREATE TABLE tb_produto ( id INT AUTO_INCREMENT PRIMARY KEY, nm_produto VARCHAR(40) NOT NULL, qtd INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query( 'INSERT INTO tb_produto (nm_produto, qtd) VALUES (?, ?), (?, ?), (?, ?)', ['Teclado', 10, 'Mouse', 4, 'Monitor', 0] ); // O `SHOW CREATE TABLE` ANTES de alterar e o ponto de recuperacao: o // `ALTER TABLE` nao tem como desfazer, e o SQL que recria a estrutura // original precisa estar anotado em algum lugar. const [antes] = await conexao.query('SHOW CREATE TABLE tb_produto'); console.log('--- a estrutura ANTES do ALTER ---'); console.log(antes[0]['Create Table']); // --- 2. `UPDATE`: alterar dados --- // // `SET` diz o que muda, `WHERE` diz ONDE. Sem `WHERE`, o `UPDATE` muda a // tabela inteira — e o banco nao pergunta se era isso que se queria. const [umRegistro] = await conexao.query( 'UPDATE tb_produto SET qtd = ? WHERE nm_produto = ?', [25, 'Teclado'] ); console.log('--- UPDATE de um registro ---'); console.log('affectedRows:', umRegistro.affectedRows, '<-- quantas linhas mudaram'); console.log('changedRows: ', umRegistro.changedRows, '<-- quantas mudaram de VALOR'); console.log('info: ', JSON.stringify(umRegistro.info)); console.log(''); console.log('`info` diz "Rows matched: 1 Changed: 1": uma linha foi encontrada, e o'); console.log('valor dela mudou. Sao tres numeros distintos, e cada um responde a uma pergunta.'); // O mesmo UPDATE com o MESMO valor: a linha bate, o valor nao muda. const [mesmoValor] = await conexao.query( 'UPDATE tb_produto SET qtd = ? WHERE nm_produto = ?', [25, 'Teclado'] ); console.log(''); console.log('o MESMO UPDATE de novo, com o MESMO valor:'); console.log(' affectedRows', mesmoValor.affectedRows, '| changedRows', mesmoValor.changedRows, '| info', JSON.stringify(mesmoValor.info)); console.log(' `affectedRows` conta a linha que bateu, nao a que mudou. Quem precisa'); console.log('saber "o dado mudou" le o `changedRows` ou o `info`.'); // --- 3. `UPDATE` sem `WHERE` --- // // O exemplo nao executa isto no banco do material: ele mostra o SQL e o // que aconteceria, porque a execucao real apagaria as tres linhas e o exemplo // perderia o proprio estado. E a aula 2 de hoje que executa o `DELETE` sem // `WHERE` de verdade, com o `TRUNCATE` ja feito e o banco recem-recriado. console.log(''); console.log('--- UPDATE sem WHERE: o que o exemplo NAO executa ---'); console.log(' UPDATE tb_produto SET qtd = 0;'); console.log(' isso muda as TRES linhas. O banco nao pergunta, nao avisa e nao desfaz.'); console.log(' A defesa e o `WHERE` explicito, sempre — inclusive quando o filtro e o id:'); console.log(' UPDATE tb_produto SET qtd = ? WHERE id = ? <- nem o WHERE precisa de id unico'); console.log(' para ser seguro, porque `WHERE qtd = 25` bate em quantas linhas der.'); // --- 4. `ALTER TABLE ADD COLUMN` --- // // Acrescentar coluna e a operacao de esquema mais comum. Ela nao perde dado // nenhum: as linhas que ja existiam recebem o valor padrao da coluna nova. await conexao.query( 'ALTER TABLE tb_produto ADD COLUMN vl_preco DECIMAL(6,2) NULL AFTER nm_produto' ); const [colunaNova] = await conexao.query('SHOW COLUMNS FROM tb_produto'); console.log(''); console.log('--- ALTER TABLE ADD COLUMN ---'); console.log('colunas agora: ' + colunaNova.map((c) => c.Field).join(', ')); const [depoisDeAdd] = await conexao.query('SELECT id, nm_produto, vl_preco, qtd FROM tb_produto ORDER BY id'); console.log('as linhas que ja existiam continuam la:'); for (const l of depoisDeAdd) { console.log(' ' + l.nm_produto + ' | preco ' + (l.vl_preco === null ? 'null' : l.vl_preco) + ' | qtd ' + l.qtd); } console.log(' a coluna nova veio `NULL` em todas: e o padrao de coluna nullable.'); console.log(' `AFTER nm_produto` coloca a coluna na posicao, e so muda a apresentacao.'); // --- 5. `ADD COLUMN ... NOT NULL DEFAULT` --- // // Com `NOT NULL` e sem `DEFAULT`, o `ALTER` em tabela com linhas FALHA: o // banco teria de inventar um valor. Com `DEFAULT`, ele preenche — e e por // isso que a coluna NOT NULL sempre vem acompanhada de DEFAULT, ou de uma // operacao em tres tempos. await conexao.query( 'ALTER TABLE tb_produto ADD COLUMN dt_cadastro DATE NOT NULL DEFAULT \'2026-05-04\'' ); // // A data e lida de um jeito que o fuso nao atrapalha: `DATE_FORMAT` traz o // texto do banco, e e ele que a pagina mostra. Um `date` lido como `Date` do // JavaScript vira `2026-05-03T22:00:00Z` no `toISOString()`, porque o servidor // esta em UTC e a maquina nao — e a aula 8 ja mediu isso. const [comPadrao] = await conexao.query( "SELECT nm_produto, DATE_FORMAT(dt_cadastro, '%Y-%m-%d') AS dt_cadastro FROM tb_produto ORDER BY id" ); console.log(''); console.log('--- ADD COLUMN NOT NULL DEFAULT ---'); console.log(' dt_cadastro NOT NULL DEFAULT data | as linhas antigas receberam o padrao:'); for (const l of comPadrao) { console.log(' ' + l.nm_produto + ' -> ' + l.dt_cadastro); } console.log(' sem o DEFAULT, o mesmo ALTER em tabela com linhas falha com ER_NO_DEFAULT_FOR_FIELD.'); console.log(' a data veio por DATE_FORMAT, e nao por toISOString: o `date` lido como'); console.log(' Date do JavaScript entra com fuso e sai no dia anterior no toISOString().'); // --- 6. `MODIFY` e `CHANGE` --- // // `MODIFY` muda tipo e restricoes sem renomear. `CHANGE` faz as duas coisas // de uma vez — e por isso que ele tem o formato de duas declaracoes. await conexao.query('ALTER TABLE tb_produto MODIFY COLUMN qtd SMALLINT NOT NULL DEFAULT 0'); const [modificada] = await conexao.query('SHOW COLUMNS FROM tb_produto'); const colunaQtd = modificada.find((c) => c.Field === 'qtd'); console.log(''); console.log('--- MODIFY: tipo e restricoes, sem renomear ---'); console.log(' qtd agora:', colunaQtd.Type, '| aceita nulo:', colunaQtd.Null === 'YES' ? 'sim' : 'nao', '| padrao:', colunaQtd.Default); console.log(' o nome continua `qtd`; o que mudou foi o tipo (int -> smallint).'); await conexao.query( 'ALTER TABLE tb_produto CHANGE COLUMN qtd qtde SMALLINT NOT NULL DEFAULT 0' ); const [renomeada] = await conexao.query('SHOW COLUMNS FROM tb_produto'); console.log(''); console.log('--- CHANGE: tipo E nome de uma vez ---'); console.log(' colunas agora: ' + renomeada.map((c) => c.Field).join(', ')); console.log(' `CHANGE COLUMN nome_antigo nome_novo tipo` — o formato tem o nome duas'); console.log(' vezes, e sem o tipo declarado a coluna perde o que tinha.'); const [comNovoNome] = await conexao.query('SELECT nm_produto, qtde FROM tb_produto ORDER BY id'); console.log(' o dado veio junto: ' + comNovoNome.map((l) => l.nm_produto + '=' + l.qtde).join(', ')); // --- 7. `DROP COLUMN` --- // // A unica operacao de estrutura que DESTRUI dado. Ela apaga a coluna e tudo // que estava nela, sem rollback e sem `IF EXISTS` util: e o comando que o // material nunca executa em exemplo, e o comando que exige backup antes. console.log(''); console.log('--- DROP COLUMN: a operacao que o exemplo NAO executa ---'); console.log(' ALTER TABLE tb_produto DROP COLUMN dt_cadastro;'); console.log(' apaga a coluna e os valores dela. Sem rollback, sem confirmacao.'); console.log(' e o material nunca executa: todo exemplo precisa rodar duas vezes seguidas'); console.log(' e dar a mesma saida, e um `DROP` de coluna destroi a base da proxima rodada.'); // --- 8. a estrutura final, e a comparacao --- const [final] = await conexao.query('SHOW CREATE TABLE tb_produto'); console.log(''); console.log('--- a estrutura DEPOIS ---'); console.log(final[0]['Create Table']); console.log(''); console.log('--- o que mudou de uma estrutura para a outra ---'); console.log(' + vl_preco DECIMAL(6,2) NULL (ADD COLUMN)'); console.log(' + dt_cadastro DATE NOT NULL (ADD COLUMN com DEFAULT)'); console.log(' ~ qtd INT -> SMALLINT (MODIFY)'); console.log(' ~ qtde nome novo (CHANGE)'); console.log(' - nenhuma coluna apagada: o exemplo mantem a tabela util para a aula 2.'); // --- 9. `RENAME TABLE` e `ALTER` sem custo de dado --- console.log(''); console.log('--- as operacoes de esquema, e o que cada uma custa ---'); console.log('ADD COLUMN | dado intacto, coluna nova recebe NULL ou o DEFAULT'); console.log('MODIFY COLUMN | dado intacto, muda tipo e/ou restricoes'); console.log('CHANGE COLUMN | dado intacto, muda tipo e nome'); console.log('RENAME TABLE | dado intacto, muda o nome da tabela'); console.log('ADD INDEX | dado intacto, so acelera'); console.log('DROP COLUMN | DESTRUI a coluna e os valores dela'); console.log(''); console.log('o backup antes de `DROP` nao e paranoia: e a unica forma de recuperar'); } finally { await conexao.end(); console.log(''); console.log('conexao encerrada com end().'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
--- a estrutura ANTES do ALTER ---
CREATE TABLE `tb_produto` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nm_produto` varchar(40) NOT NULL,
`qtd` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
--- UPDATE de um registro ---
affectedRows: 1 <-- quantas linhas mudaram
changedRows: 1 <-- quantas mudaram de VALOR
info: "Rows matched: 1 Changed: 1 Warnings: 0"
`info` diz "Rows matched: 1 Changed: 1": uma linha foi encontrada, e o
valor dela mudou. Sao tres numeros distintos, e cada um responde a uma pergunta.
o MESMO UPDATE de novo, com o MESMO valor:
affectedRows 1 | changedRows 0 | info "Rows matched: 1 Changed: 0 Warnings: 0"
`affectedRows` conta a linha que bateu, nao a que mudou. Quem precisa
saber "o dado mudou" le o `changedRows` ou o `info`.
--- UPDATE sem WHERE: o que o exemplo NAO executa ---
UPDATE tb_produto SET qtd = 0;
isso muda as TRES linhas. O banco nao pergunta, nao avisa e nao desfaz.
A defesa e o `WHERE` explicito, sempre — inclusive quando o filtro e o id:
UPDATE tb_produto SET qtd = ? WHERE id = ? <- nem o WHERE precisa de id unico
para ser seguro, porque `WHERE qtd = 25` bate em quantas linhas der.
--- ALTER TABLE ADD COLUMN ---
colunas agora: id, nm_produto, vl_preco, qtd
as linhas que ja existiam continuam la:
Teclado | preco null | qtd 25
Mouse | preco null | qtd 4
Monitor | preco null | qtd 0
a coluna nova veio `NULL` em todas: e o padrao de coluna nullable.
`AFTER nm_produto` coloca a coluna na posicao, e so muda a apresentacao.
--- ADD COLUMN NOT NULL DEFAULT ---
dt_cadastro NOT NULL DEFAULT data | as linhas antigas receberam o padrao:
Teclado -> 2026-05-04
Mouse -> 2026-05-04
Monitor -> 2026-05-04
sem o DEFAULT, o mesmo ALTER em tabela com linhas falha com ER_NO_DEFAULT_FOR_FIELD.
a data veio por DATE_FORMAT, e nao por toISOString: o `date` lido como
Date do JavaScript entra com fuso e sai no dia anterior no toISOString().
--- MODIFY: tipo e restricoes, sem renomear ---
qtd agora: smallint(6) | aceita nulo: nao | padrao: 0
o nome continua `qtd`; o que mudou foi o tipo (int -> smallint).
--- CHANGE: tipo E nome de uma vez ---
colunas agora: id, nm_produto, vl_preco, qtde, dt_cadastro
`CHANGE COLUMN nome_antigo nome_novo tipo` — o formato tem o nome duas
vezes, e sem o tipo declarado a coluna perde o que tinha.
o dado veio junto: Teclado=25, Mouse=4, Monitor=0
--- DROP COLUMN: a operacao que o exemplo NAO executa ---
ALTER TABLE tb_produto DROP COLUMN dt_cadastro;
apaga a coluna e os valores dela. Sem rollback, sem confirmacao.
e o material nunca executa: todo exemplo precisa rodar duas vezes seguidas
e dar a mesma saida, e um `DROP` de coluna destroi a base da proxima rodada.
--- a estrutura DEPOIS ---
CREATE TABLE `tb_produto` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nm_produto` varchar(40) NOT NULL,
`vl_preco` decimal(6,2) DEFAULT NULL,
`qtde` smallint(6) NOT NULL DEFAULT 0,
`dt_cadastro` date NOT NULL DEFAULT '2026-05-04',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
--- o que mudou de uma estrutura para a outra ---
+ vl_preco DECIMAL(6,2) NULL (ADD COLUMN)
+ dt_cadastro DATE NOT NULL (ADD COLUMN com DEFAULT)
~ qtd INT -> SMALLINT (MODIFY)
~ qtde nome novo (CHANGE)
- nenhuma coluna apagada: o exemplo mantem a tabela util para a aula 2.
--- as operacoes de esquema, e o que cada uma custa ---
ADD COLUMN | dado intacto, coluna nova recebe NULL ou o DEFAULT
MODIFY COLUMN | dado intacto, muda tipo e/ou restricoes
CHANGE COLUMN | dado intacto, muda tipo e nome
RENAME TABLE | dado intacto, muda o nome da tabela
ADD INDEX | dado intacto, so acelera
DROP COLUMN | DESTRUI a coluna e os valores dela
o backup antes de `DROP` nao e paranoia: e a unica forma de recuperar
conexao encerrada com end().
DELETE e os cuidado de quem apaga
DELETE e os cuidado de quem apaga
DELETE FROM t WHERE ... apaga linha a linha e conta o que saiu. TRUNCATE t
apaga a tabela inteira, zera o auto incremento e não tem WHERE. DROP TABLE
apaga a estrutura. São três comandos, três custos — e o erro de escrever o
primeiro sem o WHERE é o acidente mais caro do banco.
DELETE: o affectedRows aqui é confiável
DELETE FROM t WHERE x = 1 é apagar registro, um de cada vez, e o banco
conta. O mesmo comando sem filtro apaga tudo — e funciona: não dá erro de
sintaxe, e é por isso que o WHERE explícito é a única defesa. O TRUNCATE é o
botão de apagar tudo separado, e ele não aceita WHERE, o que impede o
filtro amplo por engano com ele.
O affectedRows do DELETE é o número de linhas removidas, e o banco
conta. O acidental mais caro não é esse: é o filtro largo demais, que apaga o
que o código não previu; a defesa contra ele é a contagem antes:
const [conta] = await conexao.query('SELECT COUNT(*) AS n FROM t WHERE x = ?', [valor]); const [del] = await conexao.query('DELETE FROM t WHERE x = ?', [valor]); del.affectedRows === conta[0].n; // true: o filtro pegou o que devia
Quando os dois números divergem, o sinal é de que o filtro pegou mais do que o
código previa. A rede de segurança do próprio SQL é o LIMIT:
DELETE FROM t WHERE id > 0 ORDER BY id LIMIT 1;
Apaga uma linha só, mesmo com filtro vago.
TRUNCATE devolve affectedRows: 0
Apagou tudo e devolveu zero. O TRUNCATE não conta — e é por isso que
affectedRows: 0 num código que decide "deu certo" passa, e o dado sumiu. A
regra que decorre: para contar o que sumiu, conta antes.
| Comando | WHERE | affectedRows | Zera auto_inc | Apaga estrutura |
|---|---|---|---|---|
DELETE FROM t WHERE x = 1 | sim | conta | não | não |
DELETE FROM t | não | conta | não | não |
TRUNCATE t | não | sempre 0 | sim | não |
DROP TABLE t | não | sempre 0 | sim | sim |
TRUNCATE é mais rápido que DELETE sem WHERE porque não escreve cada linha no
log de transação: ele marca a tabela e pronto. O preço é que **TRUNCATE não
rola** — dentro de uma transação ele causa commit implícito, e o rollback não
volta. É por isso que o DELETE é o comando dos casos que precisam de transação.
TRUNCATE zera o AUTO_INCREMENT, DELETE não
Depois de DELETE FROM t, o próximo INSERT continua depois do maior id que
existiu. Depois de TRUNCATE, o contador volta a 1. É a pegadinha que faz um
exemplo parecer quebrado na segunda execução quando o TRUNCATE não está no
começo.
Soft delete: marcar em vez de apagar
A alternativa quando o registro precisa voltar é marcar:
UPDATE t SET dt_exclusao = NOW() WHERE id = 7; SELECT ... FROM t WHERE dt_exclusao IS NULL;
A coluna dt_exclusao guarda quando foi apagado, e NULL significa "ainda está
aqui". O dado não sumiu: volta com SET dt_exclusao = NULL.
O preço é que toda consulta precisa do filtro — e o filtro esquecido uma vez
vaza dado de quem foi apagado. Por isso o soft delete só compensa quando a
volta é provável: caso contrário é só uma tabela a mais.
O outro desenho é o campo deleted: uma coluna deleted BOOLEAN NOT NULL
default 0, e a mesma lógica com menos informação. A lixeira é a versão em que
o campo deleted vira lista: as linhas marcadas ficam fora do resultado normal e
aparecem numa tela separada, de onde o usuário pode restaurar — ou apagar de
vez, e aí sim some. Restaurar é o que separa a lixeira do soft delete comum:
sem a tela de restauração, o soft delete é só um DELETE que ocupa disco.
O que não existe é desfazer. Um DELETE já confirmado não tem ROLLBACK
possível: a única forma de restaurar o que foi apagado de verdade é o backup.
Antes de um DELETE de verdade
SELECTantes, e ver o que o filtro pega. Sempre.- O mesmo
SELECTdentro de transação, e só então oDELETE. mysqldumpda tabela, com--whereno filtro, para um.sqlde backup.- Soft delete quando o registro precisa voltar;
DELETEquando não volta. LIMITno próprioDELETEquando o filtro é largo, para o estrago ter teto.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 10: `DELETE`, `TRUNCATE` e o cuidado de quem apaga. // // `DELETE FROM t WHERE ...` apaga linha a linha e conta o que saiu. `TRUNCATE t` // apaga a tabela inteira, zera o auto incremento e nao tem `WHERE`. `DROP TABLE` // apaga a estrutura. Tres comandos, tres custos, e o erro de escrever o primeiro // sem o `WHERE` e o acidente mais caro do banco. // // O exemplo EXECUTA o apaga-tudo de verdade, em tabela recem-criada, e conta as // linhas antes e depois. E o que prova que `affectedRows: 0` do `TRUNCATE` nao // quer dizer "nao apagou nada" — quer dizer "o banco nao conta". const mysql = require('mysql2/promise'); async function main() { const conexao = await mysql.createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); try { // Recriada a cada execucao: e a unica forma de o `DELETE` sem `WHERE` rodar // de verdade sem destruir o estado que a proxima rodada espera. await conexao.query('DROP TABLE IF EXISTS tb_apagar'); await conexao.query(` CREATE TABLE tb_apagar ( id INT AUTO_INCREMENT PRIMARY KEY, nm_acao VARCHAR(40) NOT NULL, qtd INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query( 'INSERT INTO tb_apagar (nm_acao, qtd) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)', ['Manter', 1, 'Apagar', 2, 'Manter', 3, 'Apagar', 4, 'Manter', 5] ); console.log('--- 5 linhas, 3 para manter e 2 para apagar ---'); // --- 1. `DELETE` com `WHERE`: o caminho normal --- // // A contagem vem ANTES do `DELETE`, e e o que permite comparar o // `affectedRows` com o que o codigo previa. A consulta de contagem e a // defesa real contra o filtro largo demais. const [paraApagar] = await conexao.query('SELECT COUNT(*) AS n FROM tb_apagar WHERE nm_acao = ?', ['Apagar']); const [del] = await conexao.query('DELETE FROM tb_apagar WHERE nm_acao = ?', ['Apagar']); const [sobrou] = await conexao.query('SELECT COUNT(*) AS n FROM tb_apagar'); console.log(''); console.log('--- DELETE com WHERE ---'); console.log('o codigo contava:', paraApagar[0].n, 'linha(s) com nm_acao = Apagar'); console.log('affectedRows: ', del.affectedRows, '<-- quantas sairam, e o banco CONTA'); console.log('linhas depois: ', sobrou[0].n); console.log('o `affectedRows` do DELETE e confiavel: ele e o numero de linhas removidas.'); console.log('e o numero que o codigo previa:', del.affectedRows === paraApagar[0].n); // --- 2. `DELETE` sem `WHERE`: apaga tudo, de verdade --- // // O exemplo EXECUTA. E a unica forma de provar o que o comando faz: a // descricao de um `DELETE` sem filtro e uma frase, a execucao e um numero. const [antesDoTotal] = await conexao.query('SELECT COUNT(*) AS n FROM tb_apagar'); const [total] = await conexao.query('DELETE FROM tb_apagar'); const [depoisDoTotal] = await conexao.query('SELECT COUNT(*) AS n FROM tb_apagar'); console.log(''); console.log('--- DELETE sem WHERE: o apaga-tudo, EXECUTADO ---'); console.log('linhas antes:', antesDoTotal[0].n); console.log('DELETE FROM tb_apagar; -> affectedRows:', total.affectedRows); console.log('linhas depois:', depoisDoTotal[0].n); console.log('o banco nao perguntou, nao avisou e nao vai desfazer. E o mesmo comando'); console.log('que o exemplo executa toda vez que roda.'); // --- 3. por que `DELETE` sem `WHERE` e tao perigoso --- // // O `WHERE` forgotten nao e erro de sintaxe, e o banco nao tem como saber a // intencao. A defesa real e o que o exemplo faz: contar antes, e comparar o // `affectedRows` com o esperado. Se o numero vier diferente do que o codigo // previa, o `rollback` desfaz — e e a aula do dia 14 que fecha isso. console.log(''); console.log('--- a defesa contra o WHERE esquecido ---'); await conexao.query( 'INSERT INTO tb_apagar (nm_acao, qtd) VALUES (?, ?), (?, ?), (?, ?)', ['Manter', 1, 'Apagar', 2, 'Manter', 3] ); const [conta] = await conexao.query('SELECT COUNT(*) AS n FROM tb_apagar WHERE nm_acao = ?', ['Apagar']); const [del2] = await conexao.query('DELETE FROM tb_apagar WHERE nm_acao = ?', ['Apagar']); console.log('antes de apagar, o proprio codigo conta:', conta[0].n, 'linha(s)'); console.log('o DELETE devolve:', del2.affectedRows); console.log('os dois numeros batem? ', del2.affectedRows === conta[0].n); console.log(' quando nao batem, o sinal e de que o filtro pegou mais do que devia.'); console.log(' E o `LIMIT` e a rede de seguranca do proprio SQL:'); const [limitado] = await conexao.query('DELETE FROM tb_apagar WHERE id > 0 ORDER BY id LIMIT 1'); console.log(' DELETE ... ORDER BY id LIMIT 1 -> affectedRows:', limitado.affectedRows, '(apaga uma so, mesmo sem filtro preciso)'); // --- 4. `TRUNCATE` --- // // `TRUNCATE` apaga tudo, sem `WHERE`, e devolve `affectedRows: 0`. Esse zero // e o detalhe que engana: o banco nao conta, e o codigo que le `affectedRows` // para decidir "deu certo" funciona. Quem precisa saber o que sumiu conta // ANTES. await conexao.query( 'INSERT INTO tb_apagar (nm_acao, qtd) VALUES (?, ?), (?, ?)', ['X', 1, 'Y', 2] ); const [antesTrunc] = await conexao.query('SELECT COUNT(*) AS n FROM tb_apagar'); const [trunc] = await conexao.query('TRUNCATE TABLE tb_apagar'); const [depoisTrunc] = await conexao.query('SELECT COUNT(*) AS n FROM tb_apagar'); console.log(''); console.log('--- TRUNCATE ---'); console.log('linhas antes:', antesTrunc[0].n, '| affectedRows:', trunc.affectedRows, '| linhas depois:', depoisTrunc[0].n); console.log(' apagou tudo e devolveu 0. O `affectedRows` do TRUNCATE nao conta nada.'); console.log(' por isso a regra: para contar o que sumiu, conta antes.'); // --- 5. `TRUNCATE` zera o `AUTO_INCREMENT`, `DELETE` nao --- // // A diferenca que mais incomoda: depois de `DELETE FROM t`, o proximo // `INSERT` continua depois do maior id que existiu. Depois de `TRUNCATE`, o // contador volta a 1. await conexao.query( 'INSERT INTO tb_apagar (nm_acao, qtd) VALUES (?, ?), (?, ?)', ['A', 1, 'B', 2] ); const [idAposDelete] = await conexao.query('SELECT MAX(id) AS n FROM tb_apagar'); await conexao.query('DELETE FROM tb_apagar'); const [depoisDeDelete] = await conexao.query( 'INSERT INTO tb_apagar (nm_acao, qtd) VALUES (?, ?)', ['C', 3] ); console.log(''); console.log('--- DELETE nao zera o auto incremento, TRUNCATE zera ---'); console.log('maior id antes:', idAposDelete[0].n); console.log('DELETE FROM tb_apagar; depois INSERT devolveu id:', depoisDeDelete.insertId, '<-- continua depois do maior'); await conexao.query('TRUNCATE TABLE tb_apagar'); const [depoisDeTrunc] = await conexao.query( 'INSERT INTO tb_apagar (nm_acao, qtd) VALUES (?, ?)', ['D', 4] ); console.log('TRUNCATE e depois INSERT devolveu id:', depoisDeTrunc.insertId, '<-- voltou a 1'); console.log(' essa e a pegadinha da aula 9: o `insertId` do exemplo comecava em 1'); console.log(' porque o `TRUNCATE` do comeco zera o contador.'); // --- 6. a comparacao dos tres comandos --- console.log(''); console.log('--- DELETE, TRUNCATE e DROP, lado a lado ---'); console.log('comando | WHERE | affectedRows | zera auto_inc | apaga estrutura'); console.log('DELETE FROM t WHERE x = 1 | sim | conta | nao | nao'); console.log('DELETE FROM t | nao | conta | nao | nao'); console.log('TRUNCATE t | nao | sempre 0 | SIM | nao'); console.log('DROP TABLE t | nao | sempre 0 | SIM | SIM'); console.log(''); console.log('`TRUNCATE` e mais rapido que `DELETE` sem `WHERE` porque nao escreve cada'); console.log('linha no log de transacao: ele marca a tabela e pronto. O preco e que'); console.log('`TRUNCATE` NAO ROLLA — em transacao, ele causa implicito e o `rollback`'); console.log('nao volta. E por isso que o `DELETE` e o comando dos casos que precisam'); console.log('de transacao (aula 14).'); // --- 7. soft delete: o campo que finge que apaga --- // // A alternativa que o material usa quando o registro precisa voltar: em vez de // apagar, marca. A coluna `dt_exclusao` guarda QUANDO foi apagado, e `NULL` // significa "ainda esta aqui". O `SELECT` filtra, e o dado continua no banco. await conexao.query( 'INSERT INTO tb_apagar (nm_acao, qtd) VALUES (?, ?), (?, ?)', ['Cliente', 1, 'Fornecedor', 2] ); await conexao.query('ALTER TABLE tb_apagar ADD COLUMN dt_exclusao DATETIME NULL'); const [soft] = await conexao.query( 'UPDATE tb_apagar SET dt_exclusao = ? WHERE nm_acao = ?', ['2026-05-04 10:00:00', 'Fornecedor'] ); const [ativos] = await conexao.query( 'SELECT nm_acao FROM tb_apagar WHERE dt_exclusao IS NULL' ); const [todos] = await conexao.query('SELECT nm_acao, dt_exclusao FROM tb_apagar ORDER BY id'); console.log(''); console.log('--- soft delete: marcar em vez de apagar ---'); console.log('UPDATE marcou', soft.affectedRows, 'linha(s) com dt_exclusao'); console.log('o que a aplicacao ve (dt_exclusao IS NULL):', ativos.map((l) => l.nm_acao).join(', ')); console.log('o que o banco guarda:'); for (const l of todos) { console.log(' ' + l.nm_acao.padEnd(12) + ' dt_exclusao: ' + (l.dt_exclusao ? l.dt_exclusao.toISOString().slice(0, 19).replace('T', ' ') + 'Z' : 'null (ativo)')); } console.log(' o dado nao sumiu: o `Fornecedor` continua la, e pode voltar com'); console.log(' UPDATE ... SET dt_exclusao = NULL. O preco e que TODA consulta precisa'); console.log(' do filtro, e que o filtro esquecido uma vez vaza dado de quem foi apagado.'); // --- 8. o backup antes de apagar --- console.log(''); console.log('--- o que fazer antes de um DELETE de verdade ---'); console.log('1. SELECT antes, e ver o que o filtro pega. Sempre.'); console.log('2. O mesmo SELECT dentro de transacao, e so entao o DELETE (aula 14).'); console.log('3. mysqldump da tabela: mysqldump --where="id = 7" banco tabela > backup.sql'); console.log('4. soft delete quando o registro precisa voltar; DELETE quando nao volta.'); console.log('5. `LIMIT` no proprio DELETE quando o filtro e largo, para o estrago ter teto.'); console.log(''); console.log('o material nao executa nenhum backup real: ele roda contra banco de teste,'); console.log('com `DROP TABLE IF EXISTS` de tabela que ele mesmo criou. Em dado de verdade,'); console.log('o `DELETE` sem `WHERE` de madrugada continua sendo o acidente mais caro.'); } finally { await conexao.end(); console.log(''); console.log('conexao encerrada com end().'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
--- 5 linhas, 3 para manter e 2 para apagar --- --- DELETE com WHERE --- o codigo contava: 2 linha(s) com nm_acao = Apagar affectedRows: 2 <-- quantas sairam, e o banco CONTA linhas depois: 3 o `affectedRows` do DELETE e confiavel: ele e o numero de linhas removidas. e o numero que o codigo previa: true --- DELETE sem WHERE: o apaga-tudo, EXECUTADO --- linhas antes: 3 DELETE FROM tb_apagar; -> affectedRows: 3 linhas depois: 0 o banco nao perguntou, nao avisou e nao vai desfazer. E o mesmo comando que o exemplo executa toda vez que roda. --- a defesa contra o WHERE esquecido --- antes de apagar, o proprio codigo conta: 1 linha(s) o DELETE devolve: 1 os dois numeros batem? true quando nao batem, o sinal e de que o filtro pegou mais do que devia. E o `LIMIT` e a rede de seguranca do proprio SQL: DELETE ... ORDER BY id LIMIT 1 -> affectedRows: 1 (apaga uma so, mesmo sem filtro preciso) --- TRUNCATE --- linhas antes: 3 | affectedRows: 0 | linhas depois: 0 apagou tudo e devolveu 0. O `affectedRows` do TRUNCATE nao conta nada. por isso a regra: para contar o que sumiu, conta antes. --- DELETE nao zera o auto incremento, TRUNCATE zera --- maior id antes: 2 DELETE FROM tb_apagar; depois INSERT devolveu id: 3 <-- continua depois do maior TRUNCATE e depois INSERT devolveu id: 1 <-- voltou a 1 essa e a pegadinha da aula 9: o `insertId` do exemplo comecava em 1 porque o `TRUNCATE` do comeco zera o contador. --- DELETE, TRUNCATE e DROP, lado a lado --- comando | WHERE | affectedRows | zera auto_inc | apaga estrutura DELETE FROM t WHERE x = 1 | sim | conta | nao | nao DELETE FROM t | nao | conta | nao | nao TRUNCATE t | nao | sempre 0 | SIM | nao DROP TABLE t | nao | sempre 0 | SIM | SIM `TRUNCATE` e mais rapido que `DELETE` sem `WHERE` porque nao escreve cada linha no log de transacao: ele marca a tabela e pronto. O preco e que `TRUNCATE` NAO ROLLA — em transacao, ele causa implicito e o `rollback` nao volta. E por isso que o `DELETE` e o comando dos casos que precisam de transacao (aula 14). --- soft delete: marcar em vez de apagar --- UPDATE marcou 1 linha(s) com dt_exclusao o que a aplicacao ve (dt_exclusao IS NULL): D, Cliente o que o banco guarda: D dt_exclusao: null (ativo) Cliente dt_exclusao: null (ativo) Fornecedor dt_exclusao: 2026-05-04 08:00:00Z o dado nao sumiu: o `Fornecedor` continua la, e pode voltar com UPDATE ... SET dt_exclusao = NULL. O preco e que TODA consulta precisa do filtro, e que o filtro esquecido uma vez vaza dado de quem foi apagado. --- o que fazer antes de um DELETE de verdade --- 1. SELECT antes, e ver o que o filtro pega. Sempre. 2. O mesmo SELECT dentro de transacao, e so entao o DELETE (aula 14). 3. mysqldump da tabela: mysqldump --where="id = 7" banco tabela > backup.sql 4. soft delete quando o registro precisa voltar; DELETE quando nao volta. 5. `LIMIT` no proprio DELETE quando o filtro e largo, para o estrago ter teto. o material nao executa nenhum backup real: ele roda contra banco de teste, com `DROP TABLE IF EXISTS` de tabela que ele mesmo criou. Em dado de verdade, o `DELETE` sem `WHERE` de madrugada continua sendo o acidente mais caro. conexao encerrada com end().