Dia 10 — SQL: alterar e apagar

Informatica · Conteudo · publicado em 30/09/2026
Dia 10 de 16

SQL: alterar e apagar

Aula 1

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

CampoPergunta que responde
affectedRowsquantas linhas o filtro encontrou
changedRowsquantas 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

ComandoO que muda
MODIFY COLUMN qtd SMALLINT NOT NULLtipo e restrições; o nome fica
CHANGE COLUMN qtd qtde SMALLINT NOT NULLtipo e nome, de uma vez
DROP COLUMN dt_cadastrodestró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.

passopor quê
ampliar antes de restringirADD COLUMN nullable → backfill → MODIFY NOT NULL
coluna nova e código novo na mesma publicaçãosenão o INSERT antigo quebra em ER_BAD_FIELD_ERROR
cada passo sozinho, e reversível por outro passoALTER 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().
Aula 2

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.

ComandoWHEREaffectedRowsZera auto_incApaga estrutura
DELETE FROM t WHERE x = 1simcontanãonão
DELETE FROM tnãocontanãonão
TRUNCATE tnãosempre 0simnão
DROP TABLE tnãosempre 0simsim

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

  1. SELECT antes, e ver o que o filtro pega. Sempre.
  2. O mesmo SELECT dentro de transação, e só então o DELETE.
  3. mysqldump da tabela, com --where no filtro, para um .sql de backup.
  4. Soft delete quando o registro precisa voltar; DELETE quando não volta.
  5. LIMIT no próprio DELETE quando 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().