Dia 11 — SQL: alterar, apagar e resumir: PHP e MySQL
UPDATE e DELETE
UPDATE
UPDATE muda o que já está gravado. A sintaxe é `UPDATE tabela SET coluna =
valor WHERE condição: sem o WHERE`, a mudança vale para todas as linhas da
tabela. String nova de texto precisa de aspas simples; número e NULL vão sem
aspas.
A forma com WHERE
UPDATE <tabela> SET <coluna> = <valor>, <coluna> = <valor> WHERE <condicao>;
O WHERE de um UPDATE aceito pode ser tão amplo quanto o de um SELECT.
Atualizar por nome, sem chave, muda todas as linhas do mesmo nome — e o servidor
não avisa.
DELETE
DELETE FROM tabela WHERE condição apaga linha a linha. Sem WHERE, apaga tudo
que está na tabela. A tabela em si continua existindo, com a estrutura intacta, e
o AUTO_INCREMENT não volta atrás.
A forma do DELETE
DELETE FROM <tabela> WHERE <condicao>;
DELETE, TRUNCATE e DROP
São três coisas diferentes e a escolha errada custa caro. DELETE pode ter
WHERE e é reversível dentro de uma transação; TRUNCATE não tem WHERE e
apaga tudo de uma vez; DROP apaga a tabela, com as colunas, os índices e a
chave primária.
As três formas de apagar
DELETE FROM <tabela> WHERE <condicao>; TRUNCATE TABLE <tabela>; -- nao aceita WHERE DROP TABLE <tabela>; -- apaga a estrutura tambem
| Comando | Aceita WHERE | O que some | Volta em transação |
|---|---|---|---|
DELETE | Sim | Só as linhas filtradas | Sim |
TRUNCATE | Não | Todas as linhas | Não |
DROP TABLE | Não | A tabela inteira | Não |
Antes de um DELETE largo, a consulta de conferência roda primeiro: o mesmo
WHERE no SELECT mostra quantas linhas caem. rowCount() devolve o número de
linhas afetadas e é a verificação mais barata que existe.
Backup antes de
DELETEsem filtro e deDROP TABLE. Não há como desfazer, e
restaurar leva mais tempo do que refazer a consulta com calma.
Exemplo
<?php declare(strict_types=1); // UPDATE, DELETE e as diferencas com TRUNCATE e DROP. // Os UPDATE e DELETE deste arquivo tm WHERE: sem ele a mudanca vale para a tabela toda. $sql = <<<'SQL' -- conferindo antes de mexer: o mesmo WHERE em SELECT mostra o que cai SELECT id, situacao, data_matricula FROM matriculas WHERE situacao = 'cancelada' AND data_matricula < '2023-01-01'; -- UPDATE de uma linha, identified pela chave primaria UPDATE alunos SET cidade = 'Campinas', atualizado_em = NOW() WHERE id = 12; -- UPDATE de varias linhas, com uma condicao que e verdadeira para um grupo UPDATE cursos SET valor = valor * 1.10 WHERE carga_horaria >= 60 AND ativo = 1; -- zerar um campo: NULL e '' sao coisas diferentes UPDATE alunos SET telefone = NULL WHERE cidade = 'Sao Paulo'; -- DELETE com filtro DELETE FROM matriculas WHERE situacao = 'cancelada' AND data_matricula < '2023-01-01'; DELETE FROM alunos WHERE id = 87; -- TRUNCATE e DROP nao aceitam WHERE: um apaga as linhas, o outro apaga a tabela -- TRUNCATE TABLE matriculas; -- DROP TABLE matriculas; SQL; echo $sql . "\n";
Saída real
-- conferindo antes de mexer: o mesmo WHERE em SELECT mostra o que cai SELECT id, situacao, data_matricula FROM matriculas WHERE situacao = 'cancelada' AND data_matricula < '2023-01-01'; -- UPDATE de uma linha, identified pela chave primaria UPDATE alunos SET cidade = 'Campinas', atualizado_em = NOW() WHERE id = 12; -- UPDATE de varias linhas, com uma condicao que e verdadeira para um grupo UPDATE cursos SET valor = valor * 1.10 WHERE carga_horaria >= 60 AND ativo = 1; -- zerar um campo: NULL e '' sao coisas diferentes UPDATE alunos SET telefone = NULL WHERE cidade = 'Sao Paulo'; -- DELETE com filtro DELETE FROM matriculas WHERE situacao = 'cancelada' AND data_matricula < '2023-01-01'; DELETE FROM alunos WHERE id = 87; -- TRUNCATE e DROP nao aceitam WHERE: um apaga as linhas, o outro apaga a tabela -- TRUNCATE TABLE matriculas; -- DROP TABLE matriculas;
ALTER TABLE e índices
Acrescentar, trocar e remover colunas
ALTER TABLE muda a estrutura de uma tabela que já tem dados, e é o comando que
roda na migração de versão. Ele faz uma operação por vez: ADD COLUMN inclui,
MODIFY muda o tipo e o DEFAULT, DROP COLUMN remove.
A sintaxe e as quatro operações
ALTER TABLE <tabela> ADD [COLUMN] <coluna> <tipo> [AFTER <outra>]; ALTER TABLE <tabela> MODIFY [COLUMN] <coluna> <tipo> [NOT NULL] [DEFAULT <valor>]; ALTER TABLE <tabela> DROP [COLUMN] <coluna>; ALTER TABLE <tabela> RENAME COLUMN <antiga> TO <nova>;
MODIFY reescreve a coluna inteira: o que não estiver na instrução é perdido.
CHANGE faz o mesmo e ainda troca o nome, porque repete o nome antigo antes do
novo — CHANGE COLUMN carga_horaria carga INT NOT NULL.
Índices e restrição em tabela existente
Índice também se cria depois, com CREATE INDEX, e some com DROP INDEX. Índice
composto tem as colunas entre parênteses, e a ordem delas define para qual filtro
ele serve.
Criar índice e acrescentar chave estrangeira
CREATE [UNIQUE] INDEX <nome> ON <tabela> (<colunas>); ALTER TABLE <tabela> ADD INDEX <nome> (<colunas>); ALTER TABLE <tabela> ADD CONSTRAINT <nome> FOREIGN KEY (<coluna>) REFERENCES <tabela_referida> (<chave>);
ADD CONSTRAINT no ALTER TABLE acrescenta chave estrangeira a uma tabela que
já existe — é a forma de corrigir um modelo antigo sem refazer os dados. Toda
migração é irreversível em parte: DROP COLUMN joga os dados fora, e o caminho
seguro é adicionar a coluna nova, copiar o dado com UPDATE, e só então remover
a antiga.
Exemplo
<?php declare(strict_types=1); // ALTER TABLE em uma tabela que ja existe. Cada comando faz uma operacao so, // e MODIFY reescreve a coluna inteira: o que nao estiver escrito e perdido. $sql = <<<'SQL' -- 1) acrescentar coluna: pode ser depois de outra, com AFTER ou logo no fim ALTER TABLE alunos ADD COLUMN telefone VARCHAR(20) NULL AFTER email; -- 2) mudar tipo e regra: repete o nome, o tipo e o NOT NULL ALTER TABLE alunos MODIFY COLUMN nome VARCHAR(160) NOT NULL; -- 3) renomear coluna, sem perder os dados ALTER TABLE alunos RENAME COLUMN nascimento TO data_nascimento; -- 4) CHANGE faz o mesmo, repetindo o tipo: util quando o tipo tambem muda ALTER TABLE alunos CHANGE COLUMN telefone telefone VARCHAR(30) NULL; -- 5) remover coluna: o dado e perdido de vez ALTER TABLE alunos DROP COLUMN telefone; -- 6) renomear a tabela ALTER TABLE cursos RENAME TO cursos_oferecidos; -- 7) indices criados depois da tabela CREATE UNIQUE INDEX ux_alunos_email ON alunos (email); CREATE INDEX ix_matriculas_curso_situacao ON matriculas (curso_id, situacao); ALTER TABLE alunos ADD INDEX ix_alunos_cidade (cidade); DROP INDEX ix_matriculas_curso_situacao ON matriculas; -- 8) restricao que faltou, acrescentada sem refazer os dados ALTER TABLE matriculas ADD CONSTRAINT fk_matricula_curso FOREIGN KEY (curso_id) REFERENCES cursos (id) ON DELETE CASCADE ON UPDATE CASCADE; SQL; echo $sql . "\n";
Saída real
-- 1) acrescentar coluna: pode ser depois de outra, com AFTER ou logo no fim ALTER TABLE alunos ADD COLUMN telefone VARCHAR(20) NULL AFTER email; -- 2) mudar tipo e regra: repete o nome, o tipo e o NOT NULL ALTER TABLE alunos MODIFY COLUMN nome VARCHAR(160) NOT NULL; -- 3) renomear coluna, sem perder os dados ALTER TABLE alunos RENAME COLUMN nascimento TO data_nascimento; -- 4) CHANGE faz o mesmo, repetindo o tipo: util quando o tipo tambem muda ALTER TABLE alunos CHANGE COLUMN telefone telefone VARCHAR(30) NULL; -- 5) remover coluna: o dado e perdido de vez ALTER TABLE alunos DROP COLUMN telefone; -- 6) renomear a tabela ALTER TABLE cursos RENAME TO cursos_oferecidos; -- 7) indices criados depois da tabela CREATE UNIQUE INDEX ux_alunos_email ON alunos (email); CREATE INDEX ix_matriculas_curso_situacao ON matriculas (curso_id, situacao); ALTER TABLE alunos ADD INDEX ix_alunos_cidade (cidade); DROP INDEX ix_matriculas_curso_situacao ON matriculas; -- 8) restricao que faltou, acrescentada sem refazer os dados ALTER TABLE matriculas ADD CONSTRAINT fk_matricula_curso FOREIGN KEY (curso_id) REFERENCES cursos (id) ON DELETE CASCADE ON UPDATE CASCADE;
GROUP BY e funções de agregação
GROUP BY
GROUP BY junta as linhas que têm o mesmo valor nas colunas indicadas e trata
cada grupo como uma linha só. O SELECT passa a listar as colunas do grupo e as
funções de agregação; sem GROUP BY, a agregação vale para a tabela inteira.
Agregação com e sem grupo
SELECT COUNT(*), SUM(<coluna>), AVG(<coluna>) FROM <tabela>; SELECT <coluna_do_grupo>, COUNT(*) FROM <tabela> GROUP BY <coluna_do_grupo>;
Sem GROUP BY a agregação devolve uma linha só, com o total da tabela inteira.
Com GROUP BY, devolve uma linha por valor distinto da coluna indicada.
As funções de agregação
As seis funções agregam muitas linhas em um valor. COUNT(*) conta linhas;
COUNT(coluna) ignora as NULL da coluna. SUM e AVG só fazem sentido em
número, e AVG de coluna com NULL considera só as preenchidas.
As seis funções e a forma de uso
| Função | Devolve |
|---|---|
COUNT(*) | Número de linhas do grupo |
COUNT(coluna) | Linhas com valor na coluna |
SUM(coluna) | Soma |
AVG(coluna) | Média |
MAX(coluna) | Maior valor |
MIN(coluna) | Menor valor |
SELECT SUM(<coluna>), AVG(<coluna>), MAX(<coluna>), MIN(<coluna>) FROM <tabela> GROUP BY <coluna>;
WHERE filtra antes, HAVING filtra depois
É a diferença que mais gera consulta errada da turma. O WHERE age sobre as
linhas da tabela, antes de agrupar: ele descarta aluno por aluno. O HAVING
age sobre os grupos, depois de agregar: ele descarta cidade por cidade. Por
isso WHERE não aceita COUNT(*) e HAVING não aceita coluna que não está no
GROUP BY.
O mesmo filtro, no lugar certo
-- WHERE: filtra a linha antes de agrupar, e por isso nao aceita COUNT SELECT <coluna>, COUNT(*) FROM <tabela> WHERE <condicao> GROUP BY <coluna>; -- HAVING: filtra o grupo depois de agregar SELECT <coluna>, COUNT(*) FROM <tabela> GROUP BY <coluna> HAVING COUNT(*) > <n>;
Quando a condição é sobre a coluna original, ela vai no WHERE e o banco filtra
antes do trabalho de agrupar — mais rápido. Quando a condição é sobre o resultado
da agregação, não há escolha: é HAVING, ou a consulta não roda.
Exemplo
<?php declare(strict_types=1); // GROUP BY e agregacao. O ponto que mais gera consulta errada: WHERE vem antes // de agrupar e filtra linha; HAVING vem depois e filtra grupo. $sql = <<<'SQL' -- agregacao sem grupo: vale para a tabela inteira SELECT COUNT(*) AS total_cursos, SUM(valor) AS faturamento, AVG(valor) AS ticket_medio, MIN(valor) AS menor, MAX(valor) AS maior FROM cursos; -- COUNT(*) conta linhas; COUNT(coluna) ignora as NULL daquela coluna SELECT COUNT(*) AS linhas, COUNT(telefone) AS com_telefone FROM alunos; -- agrupar por uma coluna e medir cada grupo SELECT cidade, COUNT(*) AS qtd_alunos, AVG(idade) AS media_idade FROM alunos WHERE ativo = 1 GROUP BY cidade ORDER BY qtd_alunos DESC; -- dois criterios de grupo: alunos matriculados por curso e por situacao SELECT c.nome AS curso, m.situacao, COUNT(*) AS qtd FROM matriculas m JOIN cursos c ON c.id = m.curso_id GROUP BY c.nome, m.situacao; -- WHERE: filtra a LINHA antes de agrupar, e por isso nao aceita COUNT SELECT situacao, COUNT(*) AS qtd FROM matriculas WHERE data_matricula >= '2024-01-01' GROUP BY situacao; -- HAVING: filtra o GRUPO depois de agregar SELECT situacao, COUNT(*) AS qtd, AVG(nota_final) AS media FROM matriculas GROUP BY situacao HAVING COUNT(*) > 10 AND AVG(nota_final) >= 7.00 ORDER BY qtd DESC; SQL; echo $sql . "\n";
Saída real
-- agregacao sem grupo: vale para a tabela inteira
SELECT COUNT(*) AS total_cursos,
SUM(valor) AS faturamento,
AVG(valor) AS ticket_medio,
MIN(valor) AS menor,
MAX(valor) AS maior
FROM cursos;
-- COUNT(*) conta linhas; COUNT(coluna) ignora as NULL daquela coluna
SELECT COUNT(*) AS linhas, COUNT(telefone) AS com_telefone FROM alunos;
-- agrupar por uma coluna e medir cada grupo
SELECT cidade, COUNT(*) AS qtd_alunos, AVG(idade) AS media_idade
FROM alunos
WHERE ativo = 1
GROUP BY cidade
ORDER BY qtd_alunos DESC;
-- dois criterios de grupo: alunos matriculados por curso e por situacao
SELECT c.nome AS curso, m.situacao, COUNT(*) AS qtd
FROM matriculas m
JOIN cursos c ON c.id = m.curso_id
GROUP BY c.nome, m.situacao;
-- WHERE: filtra a LINHA antes de agrupar, e por isso nao aceita COUNT
SELECT situacao, COUNT(*) AS qtd
FROM matriculas
WHERE data_matricula >= '2024-01-01'
GROUP BY situacao;
-- HAVING: filtra o GRUPO depois de agregar
SELECT situacao, COUNT(*) AS qtd, AVG(nota_final) AS media
FROM matriculas
GROUP BY situacao
HAVING COUNT(*) > 10 AND AVG(nota_final) >= 7.00
ORDER BY qtd DESC;
Funções de string, data e matemática no SQL
Funções de string
As funções de string recebem texto e devolvem texto ou número. CONCAT junta
vários campos em uma coluna só — com NULL em qualquer argumento, o resultado
inteiro é NULL, e por isso ela vem quase sempre junto de COALESCE.
Concatenar, cortar e medir
SELECT CONCAT(<campo>, <campo>), CONCAT_WS('<separador>', <campo>, <campo>) FROM <tabela>; SELECT UPPER(<campo>), LOWER(<campo>), LENGTH(<campo>), TRIM(<campo>) FROM <tabela>; SELECT SUBSTRING(<campo>, <inicio>, <tamanho>), LEFT(<campo>, <n>), RIGHT(<campo>, <n>) FROM <tabela>; SELECT REPLACE(<campo>, <procurado>, <troca>) FROM <tabela>;
SUBSTRING conta de 1, como no manual. TRIM sem argumento apaga os espaços das
duas pontas; TRIM(BOTH 'x' FROM campo) apaga o caractere indicado. LENGTH conta
bytes em utf8mb4, então texto com acento devolve número maior que a contagem de
letras.
Funções de data
Função de data devolve data ou número, dependendo do que se faça com ela.
NOW() e CURDATE() leem o relógio do servidor, e não o do PHP.
Extrair, formatar e comparar datas
SELECT YEAR(<data>), MONTH(<data>), DAY(<data>) FROM <tabela>; SELECT DATE_FORMAT(<data>, '<formato>') FROM <tabela>; SELECT DATEDIFF(<data1>, <data2>), DATE_ADD(<data>, INTERVAL <n> <unidade>) FROM <tabela>; SELECT CURDATE(), NOW(), LAST_DAY(CURDATE());
DATEDIFF(a, b) devolve a diferença em dias, de a para b: o resultado
fica negativo se a primeira data for anterior.
Funções matemáticas e de substituição
ROUND, CEIL e FLOOR ajustam o decimal, e ROUND(10.567, 2) arredonda em
duas casas — o segundo argumento é o número de casas, não o de dígitos.
Ajustar número e tratar vazio
SELECT ROUND(<n>, <casas>), CEIL(<n>), FLOOR(<n>), ABS(<n>) FROM <tabela>; SELECT COALESCE(<coluna1>, <coluna2>, 'padrao') FROM <tabela>; SELECT IFNULL(<coluna>, 'padrao') FROM <tabela>; SELECT CASE WHEN <condicao> THEN <resultado> ELSE <resultado> END FROM <tabela>;
COALESCE aceita quantas colunas quiser e para na primeira que não é NULL.
IFNULL aceita duas, e é o mesmo comportamento com menos opção. CASE é o
if do SQL: procura os WHEN na ordem, devolve o primeiro que fecha, e o ELSE
cobre o resto — sem ELSE, o resultado é NULL.
Exemplo
<?php declare(strict_types=1); // Funcoes de string, de data e matematicas: o que entra, o que sai. // Tudo aqui roda dentro do SELECT, sem alterar dado nenhum. $sql = <<<'SQL' -- CONCAT junta colunas; com NULL em qualquer parte o resultado inteiro e NULL SELECT CONCAT(nome, ' - ', cidade) AS identificacao, COALESCE(cidade, 'nao informada') AS local FROM alunos LIMIT 5; -- CONCAT_WS usa um separador e pula os NULL em vez de anular o resultado SELECT CONCAT_WS(' / ', nome, cidade, email) AS ficha FROM alunos LIMIT 5; -- caixa, tamanho e corte SELECT UPPER(nome) AS maiusculo, LOWER(email) AS minusculo, LENGTH(nome) AS caracteres FROM cursos; SELECT TRIM(nome) AS sem_espaco, LEFT(nome, 3) AS inicio, RIGHT(nome, 3) AS fim FROM cursos; -- SUBSTRING conta a partir de 1 SELECT SUBSTRING(nome, 1, 3) AS tres_primeiros, REPLACE(nome, ' ', '') AS junto FROM cursos; -- datas: extrair, formatar no padrao brasileiro, calcular diferenca em dias SELECT YEAR(data_matricula) AS ano, MONTH(data_matricula) AS mes, DAY(data_matricula) AS dia FROM matriculas ORDER BY data_matricula DESC; SELECT DATE_FORMAT(data_matricula, '%d/%m/%Y') AS data_br, DATEDIFF(NOW(), data_matricula) AS dias FROM matriculas ORDER BY data_matricula DESC; SELECT CURDATE() AS hoje, NOW() AS agora, DATE_ADD(CURDATE(), INTERVAL 30 DAY) AS vence_em FROM cursos; -- matematica: o segundo argumento de ROUND e o numero de casas SELECT ROUND(10.567, 2) AS arredondado, CEIL(10.1) AS topo, FLOOR(10.9) AS base, ABS(-7) AS modulo; -- tratar valor ausente e classificar com CASE SELECT IFNULL(telefone, 'nao informado') AS contato, CASE WHEN valor > 1000 THEN 'premium' ELSE 'padrao' END AS faixa FROM cursos; SQL; echo $sql . "\n";
Saída real
-- CONCAT junta colunas; com NULL em qualquer parte o resultado inteiro e NULL
SELECT CONCAT(nome, ' - ', cidade) AS identificacao, COALESCE(cidade, 'nao informada') AS local
FROM alunos
LIMIT 5;
-- CONCAT_WS usa um separador e pula os NULL em vez de anular o resultado
SELECT CONCAT_WS(' / ', nome, cidade, email) AS ficha FROM alunos LIMIT 5;
-- caixa, tamanho e corte
SELECT UPPER(nome) AS maiusculo, LOWER(email) AS minusculo, LENGTH(nome) AS caracteres
FROM cursos;
SELECT TRIM(nome) AS sem_espaco, LEFT(nome, 3) AS inicio, RIGHT(nome, 3) AS fim
FROM cursos;
-- SUBSTRING conta a partir de 1
SELECT SUBSTRING(nome, 1, 3) AS tres_primeiros, REPLACE(nome, ' ', '') AS junto
FROM cursos;
-- datas: extrair, formatar no padrao brasileiro, calcular diferenca em dias
SELECT YEAR(data_matricula) AS ano, MONTH(data_matricula) AS mes, DAY(data_matricula) AS dia
FROM matriculas
ORDER BY data_matricula DESC;
SELECT DATE_FORMAT(data_matricula, '%d/%m/%Y') AS data_br, DATEDIFF(NOW(), data_matricula) AS dias
FROM matriculas
ORDER BY data_matricula DESC;
SELECT CURDATE() AS hoje, NOW() AS agora, DATE_ADD(CURDATE(), INTERVAL 30 DAY) AS vence_em
FROM cursos;
-- matematica: o segundo argumento de ROUND e o numero de casas
SELECT ROUND(10.567, 2) AS arredondado, CEIL(10.1) AS topo, FLOOR(10.9) AS base, ABS(-7) AS modulo;
-- tratar valor ausente e classificar com CASE
SELECT IFNULL(telefone, 'nao informado') AS contato,
CASE WHEN valor > 1000 THEN 'premium' ELSE 'padrao' END AS faixa
FROM cursos;