Cheatsheet SQL e MySQL
Linguagem de consulta e gestão de bases de dados relacionais (SQL genérico e MySQL)
SQL e MySQL
SELECT e Consultas
Selecionar Tudo
SELECT * FROM clientes; -- Tabelas específicas num JOIN SELECT c.*, p.total FROM clientes c JOIN pedidos p ON p.cliente_id = c.id;
O SELECT * retorna todas as colunas. Em produção, prefira listar as colunas necessárias para reduzir tráfego e evitar expor dados sensíveis.
CONCAT e Operadores de String
-- MySQL
SELECT CONCAT(nome, ' ', apelido) AS completo
FROM clientes;
-- PostgreSQL / padrão SQL
SELECT nome || ' ' || apelido AS completo
FROM clientes;
-- Com função de formatação
SELECT CONCAT('€', FORMAT(preco, 2)) FROM produtos;O CONCAT() junta strings (MySQL). O operador || faz o mesmo em PostgreSQL/SQLite. Se qualquer valor for NULL, o resultado é NULL — use COALESCE para evitar.
EXISTS e NOT EXISTS
-- Clientes com pedidos
SELECT nome FROM clientes c
WHERE EXISTS (
SELECT 1 FROM pedidos p
WHERE p.cliente_id = c.id
);
-- Clientes sem pedidos
SELECT nome FROM clientes c
WHERE NOT EXISTS (
SELECT 1 FROM pedidos p
WHERE p.cliente_id = c.id
);O EXISTS verifica se a subquery retorna pelo menos uma linha. Mais eficiente que IN para tabelas grandes porque para na primeira correspondência. O NOT EXISTS inverte a lógica.
Colunas Específicas
SELECT nome, email, cidade
FROM clientes;
-- Com alias de coluna
SELECT nome AS cliente,
email AS contacto
FROM clientes;Selecione apenas as colunas necessárias — mais rápido e claro. O AS renomeia a coluna no resultado (alias). O alias é opcional: SELECT nome cliente também funciona.
Expressões Aritméticas
SELECT preco * quantidade AS total FROM itens; SELECT preco * (1 - desconto/100) AS preco_final FROM produtos; SELECT SUM(preco * quantidade) AS total_pedido FROM itens WHERE pedido_id = 42;
Pode calcular valores diretamente na consulta com +, -, *, / e % (módulo). Combine com AS para dar nome ao resultado.
CASE no SELECT
SELECT nome,
CASE
WHEN total > 1000 THEN 'VIP'
WHEN total > 100 THEN 'Regular'
ELSE 'Novo'
END AS segmento
FROM clientes;
-- CASE simples (comparação direta)
SELECT status,
CASE status
WHEN 1 THEN 'Ativo'
WHEN 0 THEN 'Inativo'
END AS estado
FROM contas;O CASE adiciona lógica condicional ao SELECT. A forma pesquisada usa WHEN condição; a forma simples compara um valor. O ELSE é opcional (sem ele, retorna NULL).
Alias de Tabela
SELECT c.nome, p.total FROM clientes c JOIN pedidos p ON p.cliente_id = c.id; -- Alias com AS (opcional) SELECT c.nome FROM clientes AS c;
O alias de tabela abrevia nomes longos e é essencial em JOINs e self JOINs. Uma vez definido, use o alias em todas as referências à tabela nessa consulta.
Subquery no SELECT
SELECT nome,
(SELECT COUNT(*) FROM pedidos p
WHERE p.cliente_id = c.id) AS num_pedidos
FROM clientes c;
-- Subquery escalar
SELECT nome,
(SELECT MAX(total) FROM pedidos) AS maior_pedido
FROM clientes;Uma subquery no SELECT deve retornar um único valor (escalar). É executada para cada linha da consulta exterior. Para melhor performance, prefira JOIN quando possível.
DISTINCT
-- Remove linhas duplicadas: SELECT DISTINCT cidade FROM clientes; -- Em várias colunas (combinações únicas): SELECT DISTINCT cidade, pais FROM clientes; -- Contar valores distintos: SELECT COUNT(DISTINCT cidade) FROM clientes; -- DISTINCT aplica-se a TODAS -- as colunas do SELECT
DISTINCT remove duplicados do resultado. Aplica-se ao conjunto de todas as colunas selecionadas. COUNT(DISTINCT col) conta valores únicos. Tem custo de ordenação — usa só quando necessário.
DISTINCT (Sem Duplicados)
SELECT DISTINCT cidade FROM clientes; -- Múltiplas colunas SELECT DISTINCT cidade, pais FROM clientes; -- Contar valores distintos SELECT COUNT(DISTINCT cidade) FROM clientes;
O DISTINCT remove linhas duplicadas do resultado. Com várias colunas, a combinação inteira deve ser única. Pode ser usado dentro de COUNT() para contar valores únicos.
UNION e UNION ALL
-- Combinar resultados (sem duplicados) SELECT nome FROM clientes UNION SELECT nome FROM fornecedores; -- Manter duplicados (mais rápido) SELECT cidade FROM clientes UNION ALL SELECT cidade FROM fornecedores;
O UNION combina resultados de duas consultas removendo duplicados. O UNION ALL mantém todos (mais rápido). Ambas as consultas devem ter o mesmo número de colunas e tipos compatíveis.
Aliases e subquery no FROM
-- Alias de tabela e coluna:
SELECT c.nome AS cliente,
p.total AS valor
FROM clientes AS c
JOIN pedidos AS p ON p.cliente_id = c.id;
-- Subquery no FROM (tabela derivada):
SELECT media.cidade, media.total
FROM (
SELECT cidade, AVG(total) AS total
FROM pedidos
GROUP BY cidade
) AS media
WHERE media.total > 100;Aliases (AS) encurtam nomes de tabelas e colunas. Uma subquery no FROM (tabela derivada) permite filtrar resultados agregados. O alias da subquery é obrigatório.
Filtros (WHERE)
Condição Simples
SELECT * FROM produtos WHERE preco > 100; SELECT * FROM clientes WHERE ativo = 1; SELECT * FROM pedidos WHERE data >= '2024-01-01';
O WHERE filtra linhas antes de qualquer agregação. Aceita comparações com =, >, <, >=, <=, <> (ou !=). Strings entre aspas simples.
LIKE (Padrões)
-- Começa com SELECT * FROM clientes WHERE nome LIKE 'Ana%'; -- Termina com SELECT * FROM clientes WHERE email LIKE '%@gmail.com'; -- Contém SELECT * FROM produtos WHERE nome LIKE '%phone%'; -- Um carácter qualquer SELECT * FROM clientes WHERE nome LIKE 'A_a';
O LIKE faz correspondência por padrões. O % representa qualquer sequência (0 ou mais caracteres) e o _ representa exatamente um carácter. Sensível a maiúsculas dependendo do COLLATE.
Filtro com Funções
-- Filtrar por parte da data SELECT * FROM pedidos WHERE YEAR(data) = 2024; -- Filtrar por tamanho de string SELECT * FROM clientes WHERE LENGTH(nome) > 20; -- Atenção: funções na coluna impedem índices -- Mau: WHERE YEAR(data) = 2024 -- Bom: WHERE data >= '2024-01-01' AND data < '2025-01-01'
Pode usar funções no WHERE, mas isso impede o uso de índices na coluna (full scan). Prefira comparações diretas com intervalos para manter a performance.
AND, OR e Parênteses
SELECT * FROM produtos WHERE categoria = 'eletrónica' AND preco < 500; -- OR com parênteses (precedência) SELECT * FROM clientes WHERE (cidade = 'Lisboa' OR cidade = 'Porto') AND ativo = 1;
O AND exige todas as condições; o OR exige pelo menos uma. Use parênteses para controlar a precedência — sem eles, AND tem prioridade sobre OR.
IS NULL e IS NOT NULL
-- Encontrar valores nulos SELECT * FROM clientes WHERE telefone IS NULL; -- Excluir nulos SELECT * FROM clientes WHERE email IS NOT NULL; -- ERRO: nunca use = NULL -- WHERE telefone = NULL ← não funciona!
Para verificar NULL, use sempre IS NULL ou IS NOT NULL. O operador = NULL nunca funciona porque NULL representa "desconhecido" e qualquer comparação com ele retorna UNKNOWN.
ANY, ALL e SOME
-- Maior que qualquer valor da subquery SELECT * FROM produtos WHERE preco > ANY (SELECT preco FROM promocoes); -- Maior que todos os valores SELECT * FROM produtos WHERE preco > ALL (SELECT preco FROM promocoes); -- SOME é sinónimo de ANY WHERE stock > SOME (SELECT minimo FROM alertas);
O ANY (ou SOME) retorna true se a comparação for verdadeira para pelo menos um valor. O ALL exige que seja verdadeira para todos. Usados com subqueries que retornam uma coluna.
BETWEEN (Intervalo)
SELECT * FROM produtos WHERE preco BETWEEN 10 AND 100; -- Datas SELECT * FROM pedidos WHERE data BETWEEN '2024-01-01' AND '2024-12-31'; -- Negação WHERE preco NOT BETWEEN 50 AND 200;
O BETWEEN verifica se o valor está no intervalo (inclusivo nas duas pontas). Funciona com números, datas e strings. Equivale a >= X AND <= Y.
NOT (Negação)
WHERE NOT cidade = 'Lisboa'; WHERE NOT ativo; -- Combinado WHERE NOT (preco > 100 AND stock > 0); -- Equivalente a != WHERE cidade != 'Lisboa';
O NOT nega qualquer condição. Pode ser usado com BETWEEN, IN, LIKE, EXISTS. Em muitos casos, != ou <> são equivalentes e mais legíveis.
IN (Lista de Valores)
SELECT * FROM clientes
WHERE cidade IN ('Lisboa', 'Porto', 'Braga');
-- Com subquery
SELECT * FROM pedidos
WHERE cliente_id IN (
SELECT id FROM clientes WHERE ativo = 1
);
-- Negação
WHERE status NOT IN ('cancelado', 'devolvido');O IN verifica se o valor pertence a uma lista. Mais legível que múltiplos OR. Pode conter uma subquery. O NOT IN inverte — cuidado com NULL na subquery (retorna vazio).
Comparação com NULL Seguro
-- MySQL: operador <=> SELECT * FROM clientes WHERE telefone <=> NULL; -- true se for NULL -- COALESCE para comparação WHERE COALESCE(telefone, '') = ''; -- PostgreSQL: IS NOT DISTINCT FROM WHERE telefone IS NOT DISTINCT FROM NULL;
Comparações normais falham com NULL. O operador <=> (MySQL) trata NULL como valor comparável. O COALESCE substitui NULL por um valor padrão antes de comparar.
Ordenação e Limites
ORDER BY Básico
-- Crescente (padrão) SELECT * FROM produtos ORDER BY preco ASC; -- Decrescente SELECT * FROM produtos ORDER BY preco DESC; -- Por várias colunas SELECT * FROM clientes ORDER BY cidade ASC, nome DESC;
O ORDER BY ordena o resultado. ASC é o padrão (omissível). Com várias colunas, ordena pela primeira e desempata pela segunda. Aceita alias: ORDER BY total.
FETCH FIRST (SQL Padrão)
-- SQL padrão (PostgreSQL, Oracle, SQL Server) SELECT * FROM produtos ORDER BY preco DESC FETCH FIRST 10 ROWS ONLY; -- Com offset OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- SQL Server SELECT TOP 10 * FROM produtos ORDER BY preco DESC;
O FETCH FIRST é a alternativa padrão ao LIMIT (suportado em PostgreSQL, Oracle, DB2). O SQL Server usa TOP. O MySQL usa LIMIT. Todos fazem o mesmo: restringir linhas.
ORDER BY com Expressão
-- Ordenar por cálculo SELECT nome, preco * stock AS valor FROM produtos ORDER BY preco * stock DESC; -- Ordenar por posição da coluna SELECT nome, email, cidade FROM clientes ORDER BY 3; -- NULLs primeiro/último (PostgreSQL) ORDER BY telefone NULLS FIRST;
Pode ordenar por expressões, alias ou posição da coluna (1-based). Em PostgreSQL, NULLS FIRST/NULLS LAST controla onde ficam os nulos. Em MySQL, NULL vem primeiro em ASC.
Ordenação Aleatória
-- MySQL SELECT * FROM produtos ORDER BY RAND() LIMIT 5; -- PostgreSQL SELECT * FROM produtos ORDER BY RANDOM() LIMIT 5; -- SQL Server SELECT TOP 5 * FROM produtos ORDER BY NEWID();
Para selecionar linhas aleatórias, cada SGBD tem a sua função: RAND() (MySQL), RANDOM() (PostgreSQL), NEWID() (SQL Server). Lento em tabelas grandes — considere TABLESAMPLE.
LIMIT e OFFSET
-- Primeiros 10 SELECT * FROM produtos LIMIT 10; -- Paginação: página 3 (10 por página) SELECT * FROM produtos ORDER BY id LIMIT 10 OFFSET 20; -- Sintaxe alternativa (MySQL) SELECT * FROM produtos LIMIT 20, 10;
O LIMIT restringe o número de linhas. O OFFSET salta N linhas antes de retornar. Essencial para paginação. A sintaxe LIMIT offset, count é específica do MySQL.
Ordem das Cláusulas
SELECT colunas -- 5. projeção FROM tabela -- 1. origem WHERE condição -- 2. filtro de linhas GROUP BY coluna -- 3. agrupamento HAVING condição_grupo -- 4. filtro de grupos ORDER BY coluna -- 6. ordenação LIMIT n OFFSET m; -- 7. restrição final
A ordem lógica de execução: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Por isso não pode usar alias do SELECT no WHERE.
Top N por Grupo
-- Top 3 produtos por categoria (MySQL 8+)
SELECT * FROM (
SELECT nome, categoria, preco,
ROW_NUMBER() OVER (
PARTITION BY categoria ORDER BY preco DESC
) AS pos
FROM produtos
) ranked
WHERE pos <= 3;Para obter o top N por grupo, use ROW_NUMBER() com PARTITION BY (window function). A subquery numera as linhas dentro de cada grupo; o filtro exterior seleciona as N primeiras.
JOINs
INNER JOIN
SELECT p.nome AS produto,
c.nome AS categoria
FROM produtos p
INNER JOIN categorias c
ON p.categoria_id = c.id;
-- JOIN é sinónimo de INNER JOIN
SELECT * FROM a JOIN b ON a.id = b.a_id;O INNER JOIN retorna apenas linhas com correspondência em ambas as tabelas. Linhas sem par são excluídas. A palavra INNER é opcional: JOIN sozinho faz o mesmo.
Self JOIN
-- Hierarquia: empregado → chefe
SELECT e.nome AS empregado,
c.nome AS chefe
FROM funcionarios e
JOIN funcionarios c ON e.chefe_id = c.id;
-- LEFT para incluir quem não tem chefe
SELECT e.nome, c.nome AS chefe
FROM funcionarios e
LEFT JOIN funcionarios c ON e.chefe_id = c.id;O self JOIN junta uma tabela a ela própria usando alias diferentes. Essencial para hierarquias (pai-filho), árvores e comparações entre linhas da mesma tabela.
LATERAL JOIN (PostgreSQL/MySQL 8)
-- PostgreSQL: subquery correlacionada no FROM
SELECT c.nome, top.total
FROM clientes c
JOIN LATERAL (
SELECT total FROM pedidos p
WHERE p.cliente_id = c.id
ORDER BY total DESC LIMIT 3
) top ON true;
-- MySQL 8.0.14+
SELECT c.nome, t.total
FROM clientes c
JOIN LATERAL (
SELECT total FROM pedidos
WHERE cliente_id = c.id LIMIT 3
) t ON true;O LATERAL JOIN permite que a subquery no FROM referencie colunas de tabelas anteriores. Ideal para "top N por grupo" sem window functions. O ON true é obrigatório no PostgreSQL.
LEFT JOIN
SELECT c.nome, p.total FROM clientes c LEFT JOIN pedidos p ON c.id = p.cliente_id; -- Filtrar só os que NÃO têm correspondência SELECT c.nome FROM clientes c LEFT JOIN pedidos p ON c.id = p.cliente_id WHERE p.id IS NULL;
O LEFT JOIN retorna todas as linhas da tabela esquerda, mesmo sem correspondência (colunas da direita ficam NULL). O truque WHERE p.id IS NULL encontra registos órfãos.
CROSS JOIN
-- Produto cartesiano: todas as combinações SELECT c.nome AS cor, t.nome AS tamanho FROM cores c CROSS JOIN tamanhos t; -- Equivalente implícito (sem ON) SELECT * FROM cores, tamanhos; -- Útil: gerar datas × produtos SELECT d.data, p.nome FROM datas d CROSS JOIN produtos p;
O CROSS JOIN gera o produto cartesiano: cada linha de A combinada com cada linha de B. Útil para gerar matrizes (datas × produtos, cores × tamanhos). Cuidado: N×M linhas podem ser enormes.
SELF JOIN
-- JOIN da tabela com ela própria:
-- (empregados com o nome do chefe)
SELECT e.nome AS empregado,
c.nome AS chefe
FROM empregados e
LEFT JOIN empregados c
ON e.chefe_id = c.id;
-- Aliases diferentes (e, c) são
-- obrigatórios para distinguir
-- as duas "cópias" da tabelaSELF JOIN liga uma tabela a ela própria — essencial para hierarquias (chefe/empregado, categorias pai/filho). Usa aliases diferentes para distinguir as duas referências à mesma tabela.
RIGHT JOIN e FULL OUTER JOIN
-- RIGHT: todas as linhas da tabela direita SELECT c.nome, p.total FROM pedidos p RIGHT JOIN clientes c ON p.cliente_id = c.id; -- FULL OUTER: todas de ambas (PostgreSQL/Oracle) SELECT * FROM a FULL OUTER JOIN b ON a.id = b.a_id; -- MySQL não tem FULL OUTER — usar UNION SELECT * FROM a LEFT JOIN b ON a.id = b.a_id UNION SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;
O RIGHT JOIN é o espelho do LEFT JOIN. O FULL OUTER JOIN retorna tudo de ambas as tabelas. O MySQL não suporta FULL OUTER — simule com UNION de LEFT e RIGHT.
JOIN com USING
-- Quando a coluna tem o mesmo nome SELECT * FROM pedidos JOIN clientes USING (cliente_id); -- vs ON (mais explícito) SELECT * FROM pedidos p JOIN clientes c ON p.cliente_id = c.id; -- NATURAL JOIN (automático — evitar) SELECT * FROM pedidos NATURAL JOIN clientes;
O USING é uma abreviação quando a coluna de ligação tem o mesmo nome em ambas as tabelas. O NATURAL JOIN faz isso automaticamente para todas as colunas com o mesmo nome — perigoso e pouco explícito.
Non-equi JOIN (desigualdade)
-- JOIN com condições de desigualdade: SELECT p.nome, e.nome AS escalao FROM pedidos p JOIN escaloes e ON p.total >= e.minimo AND p.total < e.maximo; -- Útil para "encaixar" valores em -- intervalos (escalões, ranges) -- Atenção: pode gerar MUITAS linhas -- (produto cartesiano parcial)
Um non-equi JOIN usa condições de desigualdade (maior, menor, BETWEEN) em vez de igualdade. Ideal para mapear valores para intervalos (escalões de preço, comissões). Atenção ao volume de linhas gerado.
JOIN com Múltiplas Condições
SELECT *
FROM vendas v
JOIN produtos p
ON v.produto_id = p.id
AND v.ano = p.ano_venda;
-- JOIN com condição extra
JOIN clientes c
ON v.cliente_id = c.id
AND c.pais = 'Portugal';O ON aceita múltiplas condições com AND. Útil para tabelas com chaves compostas. Condições de filtro no ON vs WHERE comportam-se diferente em LEFT JOIN (filtros no ON não eliminam linhas da esquerda).
Múltiplos JOINs
SELECT p.nome AS produto,
c.nome AS categoria,
f.nome AS fornecedor
FROM produtos p
JOIN categorias c ON p.categoria_id = c.id
JOIN fornecedores f ON p.fornecedor_id = f.id
WHERE p.ativo = 1;Pode encadear vários JOINs na mesma consulta. Cada JOIN adiciona uma tabela. Mantenha os alias organizados e verifique que as condições ON estão corretas para evitar produtos cartesianos acidentais.
Agregações
COUNT
-- Contar todas as linhas SELECT COUNT(*) FROM clientes; -- Contar não-nulos de uma coluna SELECT COUNT(email) FROM clientes; -- Contar valores distintos SELECT COUNT(DISTINCT cidade) FROM clientes;
O COUNT(*) conta todas as linhas (incluindo NULL). O COUNT(coluna) ignora nulos. O COUNT(DISTINCT col) conta valores únicos. É a única função que nunca retorna NULL.
HAVING (Filtro de Grupo)
SELECT cidade, COUNT(*) AS total FROM clientes GROUP BY cidade HAVING COUNT(*) > 10; -- HAVING com agregação SELECT categoria, AVG(preco) AS media FROM produtos GROUP BY categoria HAVING AVG(preco) > 50;
O HAVING filtra grupos após o GROUP BY (o WHERE filtra linhas antes). Use WHERE para condições de linha e HAVING para condições de agregação. Pode usar alias: HAVING total > 10.
HAVING (filtrar agregações)
-- WHERE não pode filtrar agregados; -- HAVING sim (após o GROUP BY): SELECT cliente_id, SUM(total) AS gasto FROM pedidos GROUP BY cliente_id HAVING SUM(total) > 500; -- Ordem lógica: -- WHERE filtra linhas ANTES -- HAVING filtra grupos DEPOIS SELECT categoria, COUNT(*) AS n FROM produtos WHERE ativo = TRUE -- antes GROUP BY categoria HAVING COUNT(*) >= 5; -- depois
HAVING filtra resultados agregados (após o GROUP BY), enquanto WHERE filtra linhas antes da agregação. Regra: WHERE primeiro, HAVING depois. Não dá para usar agregados no WHERE.
SUM, AVG e Aritmética
SELECT SUM(total) AS receita FROM pedidos; SELECT AVG(preco) AS media FROM produtos; -- Média com NULL tratado como 0 SELECT AVG(COALESCE(desconto, 0)) FROM itens; -- Soma condicional SELECT SUM(CASE WHEN status = 'pago' THEN total ELSE 0 END) FROM pedidos;
O SUM soma valores e o AVG calcula a média. Ambos ignoram NULL. Para soma condicional, combine com CASE. Se todas as linhas forem NULL, o resultado é NULL (não 0).
GROUP BY com ROLLUP
-- MySQL / SQL Server SELECT categoria, ano, SUM(total) FROM vendas GROUP BY categoria, ano WITH ROLLUP; -- PostgreSQL (GROUPING SETS) SELECT categoria, ano, SUM(total) FROM vendas GROUP BY ROLLUP (categoria, ano);
O ROLLUP adiciona linhas de subtotal e total geral ao resultado. Com GROUP BY ROLLUP(a, b) obtém grupos por (a,b), por (a) e o total geral. Linhas de subtotal têm NULL nas colunas agregadas.
GROUP BY múltiplas colunas
-- Agrupar por várias colunas: SELECT pais, cidade, COUNT(*) AS total FROM clientes GROUP BY pais, cidade ORDER BY pais, total DESC; -- Cada combinação única de -- (pais, cidade) vira uma linha -- Com ROLLUP (subtotais + total): SELECT pais, cidade, COUNT(*) FROM clientes GROUP BY pais, cidade WITH ROLLUP; -- (MySQL; PostgreSQL: ROLLUP(pais, cidade))
GROUP BY com várias colunas cria um grupo por cada combinação única. WITH ROLLUP (MySQL) ou ROLLUP() (PostgreSQL) adiciona linhas de subtotal e total geral — útil para relatórios.
MIN e MAX
SELECT MIN(preco), MAX(preco) FROM produtos; -- Data mais recente SELECT MAX(data) AS ultimo_pedido FROM pedidos; -- String: ordem alfabética SELECT MIN(nome), MAX(nome) FROM clientes;
O MIN e MAX retornam o menor e maior valor. Funcionam com números, datas e strings (ordem alfabética). Ignoram NULL. Úteis para encontrar limites e datas extremas.
GROUP_CONCAT (MySQL)
-- MySQL: concatenar valores do grupo
SELECT categoria,
GROUP_CONCAT(nome ORDER BY nome SEPARATOR ', ')
FROM produtos
GROUP BY categoria;
-- PostgreSQL: STRING_AGG
SELECT categoria,
STRING_AGG(nome, ', ' ORDER BY nome)
FROM produtos
GROUP BY categoria;O GROUP_CONCAT (MySQL) junta valores do grupo numa string. O STRING_AGG faz o mesmo em PostgreSQL. Útil para listas de tags, nomes ou IDs. Limite de tamanho: group_concat_max_len.
GROUP BY
SELECT cidade, COUNT(*) AS total FROM clientes GROUP BY cidade; -- Múltiplas colunas SELECT ano, mes, SUM(valor) AS total FROM vendas GROUP BY ano, mes ORDER BY ano, mes;
O GROUP BY agrupa linhas com o mesmo valor e aplica agregações a cada grupo. Todas as colunas no SELECT que não são agregadas devem estar no GROUP BY (em modo ONLY_FULL_GROUP_BY).
Agregação com JOIN
SELECT c.nome,
COUNT(p.id) AS num_pedidos,
COALESCE(SUM(p.total), 0) AS total_gasto
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nome
ORDER BY total_gasto DESC;Combine JOIN com GROUP BY para agregar dados relacionados. Use LEFT JOIN para incluir clientes sem pedidos. O COALESCE converte NULL (sem pedidos) em 0.
INSERT, UPDATE, DELETE
INSERT Simples
INSERT INTO clientes (nome, email, cidade)
VALUES ('Ana Silva', 'ana@mail.com', 'Lisboa');
-- Com valores padrão
INSERT INTO produtos (nome, preco)
VALUES ('Teclado', DEFAULT);O INSERT adiciona linhas. Especifique sempre as colunas explicitamente (não confie na ordem). O DEFAULT usa o valor padrão da coluna. O id com AUTO_INCREMENT é gerado automaticamente.
UPDATE Básico
UPDATE produtos
SET preco = 99.90
WHERE id = 5;
-- Múltiplas colunas
UPDATE clientes
SET email = 'novo@mail.com',
atualizado_em = NOW()
WHERE id = 10;O UPDATE modifica linhas existentes. Use sempre WHERE para limitar as linhas afetadas — sem ele, atualiza todas. Teste antes com SELECT usando a mesma condição.
REPLACE (MySQL)
-- Apaga e reinsere se a chave existir REPLACE INTO produtos (id, nome, preco) VALUES (5, 'Rato Pro', 39.90); -- Equivalente a: -- DELETE + INSERT (se chave duplicada) -- INSERT normal (se não existir)
O REPLACE (MySQL) apaga a linha existente e insere uma nova se houver conflito de chave. Diferente do upsert: colunas não especificadas ficam com valores padrão (não são mantidas). Prefira ON DUPLICATE KEY UPDATE.
INSERT Múltiplo
INSERT INTO produtos (nome, preco, stock)
VALUES
('Rato', 25.90, 100),
('Teclado', 49.90, 50),
('Monitor', 299.00, 20);Insira várias linhas num único INSERT separando os conjuntos de valores por vírgulas. Muito mais rápido que múltiplos INSERTs individuais (menos round-trips ao servidor).
UPDATE com Cálculo e JOIN
-- Aumentar 10% UPDATE produtos SET preco = preco * 1.10 WHERE categoria = 'eletrónica'; -- UPDATE com JOIN (MySQL) UPDATE pedidos p JOIN clientes c ON p.cliente_id = c.id SET p.desconto = 10 WHERE c.vip = 1; -- PostgreSQL UPDATE pedidos SET desconto = 10 FROM clientes c WHERE cliente_id = c.id AND c.vip = 1;
O UPDATE pode usar a coluna atual (preco = preco * 1.1) e JOINs para atualizar com base noutra tabela. A sintaxe de UPDATE JOIN varia entre MySQL e PostgreSQL.
INSERT de múltiplas linhas
-- Várias linhas num só INSERT:
INSERT INTO produtos (nome, preco)
VALUES
('Teclado', 45.00),
('Rato', 25.50),
('Monitor', 180.00);
-- Muito mais rápido que N INSERTs
-- (uma única transação)
-- INSERT a partir de SELECT:
INSERT INTO produtos_arquivo
(nome, preco)
SELECT nome, preco
FROM produtos
WHERE descontinuado = TRUE;Um INSERT com várias linhas (VALUES (...), (...)) é muito mais rápido que INSERTs separados (uma transação só). INSERT ... SELECT copia dados de outra tabela sem sair do SQL.
INSERT a partir de SELECT
-- Copiar dados entre tabelas INSERT INTO arquivo_pedidos SELECT * FROM pedidos WHERE ano < 2020; -- Com colunas específicas INSERT INTO relatorio (nome, total) SELECT nome, SUM(total) FROM pedidos GROUP BY nome;
O INSERT ... SELECT copia resultados de uma consulta para outra tabela. As colunas devem ser compatíveis em tipo e ordem. Ideal para arquivos, relatórios e tabelas de resumo.
DELETE
DELETE FROM clientes WHERE ativo = 0; -- Com JOIN (MySQL) DELETE p FROM pedidos p JOIN clientes c ON p.cliente_id = c.id WHERE c.bloqueado = 1; -- Limpar tabela antiga DELETE FROM logs WHERE data < '2023-01-01' LIMIT 10000;
O DELETE remove linhas. Use sempre WHERE — sem ele apaga tudo. O LIMIT no DELETE (MySQL) permite apagar em lotes para não bloquear a tabela. Em PostgreSQL, use DELETE ... USING para JOINs.
UPDATE com JOIN
-- Atualizar com base noutra tabela: -- MySQL: UPDATE pedidos p JOIN clientes c ON c.id = p.cliente_id SET p.desconto = 10 WHERE c.vip = TRUE; -- PostgreSQL / SQL padrão: UPDATE pedidos SET desconto = 10 FROM clientes WHERE pedidos.cliente_id = clientes.id AND clientes.vip = TRUE; -- A sintaxe varia entre SGBDs!
UPDATE com JOIN atualiza linhas com base noutra tabela. A sintaxe difere: MySQL usa UPDATE ... JOIN; PostgreSQL usa a cláusula FROM. Verifica a documentação do teu SGBD.
INSERT ON DUPLICATE KEY (Upsert)
-- MySQL: inserir ou atualizar
INSERT INTO stats (produto_id, visualizacoes)
VALUES (42, 1)
ON DUPLICATE KEY UPDATE
visualizacoes = visualizacoes + 1;
-- PostgreSQL: ON CONFLICT
INSERT INTO stats (produto_id, visualizacoes)
VALUES (42, 1)
ON CONFLICT (produto_id)
DO UPDATE SET visualizacoes = stats.visualizacoes + 1;O upsert insere se não existir, ou atualiza se já existir. No MySQL use ON DUPLICATE KEY UPDATE; no PostgreSQL use ON CONFLICT ... DO UPDATE. Requer uma chave única/primária.
TRUNCATE vs DELETE
-- TRUNCATE: remove tudo, reinicia AUTO_INCREMENT TRUNCATE TABLE logs; -- DELETE sem WHERE: remove tudo (mais lento) DELETE FROM logs; -- TRUNCATE não dispara triggers -- TRUNCATE não pode ter WHERE -- TRUNCATE é DDL (não transacional em MySQL)
O TRUNCATE remove todas as linhas rapidamente e reinicia o AUTO_INCREMENT. Não aceita WHERE, não dispara triggers e em MySQL não é transacional. Use DELETE quando precisar de condições ou rollback.
Criar Tabelas e Tipos
CREATE TABLE
CREATE TABLE produtos (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
preco DECIMAL(8,2) DEFAULT 0.00,
stock INT UNSIGNED DEFAULT 0,
ativo BOOLEAN DEFAULT TRUE,
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);O CREATE TABLE define colunas com tipo e constraints. O PRIMARY KEY identifica cada linha. O DEFAULT define o valor padrão. O AUTO_INCREMENT (MySQL) gera IDs sequenciais automaticamente.
Constraints de Integridade
CREATE TABLE contas (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
saldo DECIMAL(10,2) CHECK (saldo >= 0),
tipo ENUM('pessoal', 'empresa') DEFAULT 'pessoal',
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Constraints: NOT NULL (obrigatório), UNIQUE (sem duplicados), CHECK (validação), DEFAULT (valor padrão), ENUM (valores permitidos). Garantem integridade ao nível da BD.
CREATE TABLE AS SELECT
-- Criar tabela a partir de consulta CREATE TABLE clientes_vip AS SELECT nome, email, total_gasto FROM clientes WHERE total_gasto > 1000; -- Só estrutura (sem dados) CREATE TABLE nova LIKE clientes;
O CREATE TABLE AS SELECT (CTAS) cria uma tabela com os resultados de uma consulta. Não copia índices nem constraints — só dados e tipos. O LIKE copia a estrutura completa (MySQL).
Tipos Numéricos
-- Inteiros TINYINT (1 byte: -128 a 127) SMALLINT (2 bytes) INT (4 bytes: -2B a 2B) BIGINT (8 bytes) -- Decimais DECIMAL(10,2) -- exato (dinheiro!) FLOAT -- aproximado (4 bytes) DOUBLE -- aproximado (8 bytes)
Use DECIMAL para valores monetários (exato). FLOAT/DOUBLE são aproximados (erros de arredondamento). INT chega para a maioria dos IDs. UNSIGNED duplica o limite positivo.
Chave Primária e Estrangeira
CREATE TABLE pedidos (
id INT PRIMARY KEY AUTO_INCREMENT,
cliente_id INT NOT NULL,
total DECIMAL(8,2),
FOREIGN KEY (cliente_id)
REFERENCES clientes(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);A PRIMARY KEY identifica cada linha (única + não nula). A FOREIGN KEY liga a outra tabela. ON DELETE CASCADE apaga pedidos se o cliente for apagado. ON DELETE SET NULL põe NULL em vez de apagar.
Tipos Especiais (ENUM, JSON, UUID)
-- ENUM: valores fixos
status ENUM('pendente', 'pago', 'enviado')
-- JSON (MySQL 5.7+, PostgreSQL)
dados JSON,
SELECT dados->>'$.nome' FROM tabela;
-- UUID (PostgreSQL)
id UUID DEFAULT gen_random_uuid()
-- BOOLEAN (alias de TINYINT(1) no MySQL)
ativo BOOLEAN DEFAULT TRUEO ENUM restringe a valores fixos (eficiente mas rígido). O tipo JSON guarda documentos flexíveis. O UUID é um identificador global único (128 bits). O BOOLEAN no MySQL é TINYINT(1) internamente.
Tipos de Texto
CHAR(10) -- tamanho fixo (códigos, siglas) VARCHAR(255) -- tamanho variável (nomes, emails) TEXT -- até 64KB (descrições) MEDIUMTEXT -- até 16MB LONGTEXT -- até 4GB -- PostgreSQL VARCHAR(n), TEXT, CHAR(n)
O CHAR tem tamanho fixo (preenche com espaços). O VARCHAR armazena só o necessário. TEXT para textos longos (sem limite prático). Em PostgreSQL, TEXT e VARCHAR têm performance igual.
ALTER TABLE
-- Adicionar coluna ALTER TABLE clientes ADD telefone VARCHAR(20); -- Remover coluna ALTER TABLE clientes DROP COLUMN fax; -- Modificar tipo (MySQL) ALTER TABLE produtos MODIFY preco DECIMAL(10,2); -- PostgreSQL ALTER TABLE produtos ALTER COLUMN preco TYPE NUMERIC(10,2); -- Renomear coluna ALTER TABLE clientes RENAME COLUMN nome TO nome_completo;
O ALTER TABLE modifica a estrutura: adicionar (ADD), remover (DROP), alterar tipo (MODIFY/ALTER COLUMN) ou renomear colunas. Em tabelas grandes pode ser lento (recria a tabela).
CREATE TABLE AS
-- Criar tabela a partir de uma query: CREATE TABLE clientes_vip AS SELECT id, nome, email FROM clientes WHERE total_gasto > 1000; -- Copiar estrutura + dados de outra: CREATE TABLE clientes_backup AS SELECT * FROM clientes; -- Só a estrutura (sem dados): CREATE TABLE nova AS SELECT * FROM clientes WHERE 1 = 0; -- A nova tabela NÃO herda índices, -- constraints nem chaves primárias
CREATE TABLE AS SELECT (CTAS) cria uma tabela a partir do resultado de uma query, copiando estrutura e dados. Não herda índices, constraints ou chaves — adiciona-os depois se necessário.
Tipos de Data e Hora
DATE -- '2024-01-15' TIME -- '14:30:00' DATETIME -- '2024-01-15 14:30:00' TIMESTAMP -- como DATETIME + fuso horário YEAR -- 2024 -- PostgreSQL DATE, TIME, TIMESTAMP, TIMESTAMPTZ, INTERVAL
O DATE guarda só a data. O DATETIME guarda data e hora. O TIMESTAMP converte para UTC ao guardar (bom para apps multi-fuso). Escolha o tipo mais restritivo que cubra a necessidade.
DROP e RENAME TABLE
-- Apagar tabela (irreversível!) DROP TABLE IF EXISTS temp_logs; -- Renomear tabela RENAME TABLE clientes TO clientes_antigos; -- MySQL: múltiplas RENAME TABLE a TO a_new, b TO b_new; -- PostgreSQL ALTER TABLE clientes RENAME TO clientes_antigos;
O DROP TABLE elimina a tabela e todos os dados. O IF EXISTS evita erro se não existir. O RENAME muda o nome sem perder dados. Cuidado: DROP é irreversível (sem ROLLBACK em DDL no MySQL).
Tabelas temporárias
-- Existe só na sessão atual: CREATE TEMPORARY TABLE tmp_totais ( cliente_id INT, total DECIMAL(10,2) ); INSERT INTO tmp_totais SELECT cliente_id, SUM(total) FROM pedidos GROUP BY cliente_id; -- Usar como qualquer tabela: SELECT * FROM tmp_totais WHERE total > 100; -- Apagada automaticamente quando -- a sessão/ligação termina DROP TEMPORARY TABLE IF EXISTS tmp_totais;
Tabelas TEMPORARY existem apenas na sessão atual e são apagadas ao fechar a ligação. Ideais para resultados intermédios complexos. Só são visíveis para quem as criou.
Views, Índices e Transações
Criar e Usar VIEW
CREATE VIEW clientes_ativos AS SELECT id, nome, email FROM clientes WHERE ativo = 1; -- Usar como tabela SELECT * FROM clientes_ativos WHERE cidade = 'Lisboa'; -- Apagar DROP VIEW IF EXISTS clientes_ativos;
Uma VIEW é uma consulta guardada que se usa como tabela virtual. Não armazena dados (executa a query ao consultar). Útil para simplificar consultas complexas e restringir acesso a colunas.
Transações (ACID)
START TRANSACTION; UPDATE contas SET saldo = saldo - 100 WHERE id = 1; UPDATE contas SET saldo = saldo + 100 WHERE id = 2; -- Se tudo correu bem: COMMIT; -- Se houve erro: ROLLBACK;
Uma transação agrupa operações atómicas: ou todas succeedem (COMMIT) ou nenhuma se aplica (ROLLBACK). Garante consistência (ex: transferências). Requer engine transacional (InnoDB no MySQL).
CTE Recursiva
WITH RECURSIVE hierarquia AS (
-- Âncora: nível raiz
SELECT id, nome, chefe_id, 1 AS nivel
FROM funcionarios WHERE chefe_id IS NULL
UNION ALL
-- Recursão: filhos
SELECT f.id, f.nome, f.chefe_id, h.nivel + 1
FROM funcionarios f
JOIN hierarquia h ON f.chefe_id = h.id
)
SELECT * FROM hierarquia ORDER BY nivel;A CTE recursiva (WITH RECURSIVE) referencia-se a si própria. Tem uma parte âncora (caso base) e uma recursiva. Ideal para hierarquias, árvores e grafos. Requer UNION ALL entre as partes.
CTE (WITH)
-- Common Table Expression: WITH vendas_2024 AS ( SELECT cliente_id, SUM(total) AS total FROM pedidos WHERE YEAR(data) = 2024 GROUP BY cliente_id ) SELECT c.nome, v.total FROM vendas_2024 v JOIN clientes c ON c.id = v.cliente_id WHERE v.total > 1000; -- Mais legível que subqueries -- aninhadas; pode encadear várias: -- WITH a AS (...), b AS (...)
CTE (WITH nome AS (...)) cria uma tabela temporária nomeada para a query — muito mais legível que subqueries aninhadas. Podes encadear várias CTEs separadas por vírgula. Suportado em todos os SGBDs modernos.
VIEW Atualizável
-- VIEW simples (atualizável)
CREATE VIEW vw_produtos AS
SELECT id, nome, preco FROM produtos WHERE ativo = 1;
-- INSERT/UPDATE através da VIEW
INSERT INTO vw_produtos (nome, preco) VALUES ('Novo', 10);
UPDATE vw_produtos SET preco = 15 WHERE id = 5;
-- Com WITH CHECK OPTION (validação)
CREATE VIEW vw_ativos AS
SELECT * FROM clientes WHERE ativo = 1
WITH CHECK OPTION;VIEWs simples (sem JOIN, GROUP BY, DISTINCT) são atualizáveis. O WITH CHECK OPTION impede inserir/atualizar linhas que não cumpram a condição da VIEW.
SAVEPOINT
START TRANSACTION; INSERT INTO pedidos (cliente_id, total) VALUES (1, 50); SAVEPOINT depois_pedido; INSERT INTO itens (pedido_id, produto_id) VALUES (99, 5); -- Erro no segundo INSERT: reverter só até ao savepoint ROLLBACK TO depois_pedido; -- O pedido mantém-se, o item não COMMIT;
O SAVEPOINT cria um ponto intermédio na transação. O ROLLBACK TO reverte só até esse ponto (não desfaz tudo). Útil para operações em lote onde um erro não deve anular o trabalho anterior.
Window Functions (Básico)
SELECT nome, departamento, salario,
RANK() OVER (PARTITION BY departamento ORDER BY salario DESC) AS rank,
AVG(salario) OVER (PARTITION BY departamento) AS media_dept
FROM funcionarios;
-- ROW_NUMBER vs RANK vs DENSE_RANK
ROW_NUMBER() -- 1,2,3,4 (sem empates)
RANK() -- 1,1,3,4 (salta)
DENSE_RANK() -- 1,1,2,3 (não salta)Window functions calculam sobre um "grupo" sem colapsar linhas. O PARTITION BY define o grupo; o ORDER BY a ordem. RANK, ROW_NUMBER e DENSE_RANK diferem no tratamento de empates.
Window functions
-- Agregação SEM agrupar linhas:
SELECT nome, departamento, salario,
AVG(salario) OVER (
PARTITION BY departamento
) AS media_dept,
salario - AVG(salario) OVER (
PARTITION BY departamento
) AS diff
FROM empregados;
-- Ranking:
SELECT nome,
ROW_NUMBER() OVER (
ORDER BY salario DESC
) AS posicao
FROM empregados;
-- OVER() define a "janela";
-- PARTITION BY divide em gruposWindow functions calculam agregados sobre um grupo de linhas sem as colapsar (ao contrário do GROUP BY). OVER (PARTITION BY ...) define a janela. Inclui ROW_NUMBER, RANK, LAG, LEAD. MySQL 8+ e PostgreSQL.
Criar e Gerir Índices
-- Índice simples CREATE INDEX idx_email ON clientes(email); -- Índice composto CREATE INDEX idx_cat_preco ON produtos(categoria, preco); -- Índice único CREATE UNIQUE INDEX idx_cpf ON clientes(cpf); -- Apagar DROP INDEX idx_email ON clientes; -- MySQL DROP INDEX idx_email; -- PostgreSQL
Índices aceleram WHERE, JOIN e ORDER BY. O índice composto funciona para a primeira coluna (ou ambas em ordem). O UNIQUE também garante unicidade. Índices ocupam espaço e atrasam INSERT/UPDATE.
Subqueries (FROM, WHERE, SELECT)
-- No WHERE (IN)
SELECT nome FROM clientes
WHERE id IN (SELECT cliente_id FROM pedidos);
-- No FROM (tabela derivada)
SELECT categoria, media FROM (
SELECT categoria, AVG(preco) AS media
FROM produtos GROUP BY categoria
) t WHERE media > 50;
-- Correlacionada
SELECT nome, (
SELECT COUNT(*) FROM pedidos p
WHERE p.cliente_id = c.id
) AS num_pedidos FROM clientes c;Subqueries podem estar no WHERE (filtro), no FROM (tabela derivada) ou no SELECT (escalar). A correlacionada referencia a query exterior (executa por linha). Prefira JOIN quando possível para performance.
Window Functions (Avançado)
SELECT mes, total,
LAG(total, 1) OVER (ORDER BY mes) AS mes_anterior,
LEAD(total, 1) OVER (ORDER BY mes) AS mes_seguinte,
SUM(total) OVER (ORDER BY mes) AS acumulado,
total * 100.0 / SUM(total) OVER () AS percentagem
FROM vendas_mensais;LAG/LEAD acedem a linhas anteriores/seguintes. O SUM() OVER (ORDER BY) calcula totais acumulados. O OVER () vazio aplica a toda a tabela. Substitui auto-JOINs complexos.
EXPLAIN (Plano de Execução)
EXPLAIN SELECT * FROM produtos WHERE categoria = 'eletrónica' AND preco > 100; -- MySQL: formato JSON EXPLAIN FORMAT=JSON SELECT ...; -- PostgreSQL: com métricas reais EXPLAIN ANALYZE SELECT ...;
O EXPLAIN mostra como a consulta será executada: se usa índice, quantas linhas examina, tipo de scan. type: ALL = full scan (mau). type: ref/range = usa índice (bom). O ANALYZE executa e mostra tempos reais.
CTE (Common Table Expression)
WITH vendas_mensais AS (
SELECT MONTH(data) AS mes, SUM(total) AS total
FROM pedidos
WHERE YEAR(data) = 2024
GROUP BY MONTH(data)
)
SELECT mes, total,
total - LAG(total) OVER (ORDER BY mes) AS diferenca
FROM vendas_mensais;A CTE (WITH) cria uma tabela temporária nomeada para a consulta. Mais legível que subqueries aninhadas. Pode ser referenciada múltiplas vezes. Suportada em MySQL 8+, PostgreSQL, SQL Server.
Prepared Statements
-- MySQL
PREPARE stmt FROM 'SELECT * FROM clientes WHERE id = ?';
SET @id = 42;
EXECUTE stmt USING @id;
DEALLOCATE PREPARE stmt;
-- Na aplicação (PHP/PDO)
-- $stmt = $pdo->prepare('SELECT * FROM clientes WHERE id = ?');
-- $stmt->execute([$id]);Prepared statements separam SQL de dados: previnem SQL injection e permitem reutilizar o plano de execução. O ? é o placeholder. Na prática, use o driver da linguagem (PDO, psycopg2) em vez de SQL puro.
Funções Úteis
Funções de String
UPPER('olá') -- 'OLÁ'
LOWER('OLÁ') -- 'olá'
LENGTH('olá') -- 4 (bytes em MySQL)
CHAR_LENGTH('olá') -- 4 (caracteres)
TRIM(' olá ') -- 'olá'
SUBSTRING('olá', 1, 2) -- 'ol'
REPLACE('olá', 'á', 'a') -- 'ola'
LEFT('olá', 2) -- 'ol'
RIGHT('olá', 2) -- 'lá'Funções de manipulação de texto. O LENGTH conta bytes; o CHAR_LENGTH conta caracteres (importante com acentos/UTF-8). O SUBSTRING(str, pos, len) é 1-based. O TRIM remove espaços nas pontas.
CAST e CONVERT
-- Conversão explícita de tipo
SELECT CAST('123' AS INT);
SELECT CAST(preco AS CHAR) FROM produtos;
SELECT CAST('2024-01-15' AS DATE);
-- MySQL: CONVERT
SELECT CONVERT(nome, CHAR) FROM clientes;
-- PostgreSQL: :: (shorthand)
SELECT '123'::INT, preco::TEXT FROM produtos;O CAST converte entre tipos explicitamente (padrão SQL). O CONVERT é alternativo (MySQL). Em PostgreSQL, o operador :: é mais conciso. Conversões implícitas podem causar perda de precisão.
REGEXP (Expressões Regulares)
-- MySQL
SELECT * FROM clientes
WHERE email REGEXP '^[a-z]+@[a-z]+\\.com$';
-- PostgreSQL (~ operator)
SELECT * FROM clientes
WHERE email ~ '^[a-z]+@[a-z]+\.com$';
-- MySQL 8: REGEXP_LIKE
WHERE REGEXP_LIKE(telefone, '^[0-9]{9}$');O REGEXP (MySQL) ou ~ (PostgreSQL) faz correspondência por expressões regulares. Mais poderoso que LIKE mas mais lento. Use ^ (início), $ (fim), [0-9] (classe), {n} (repetição).
Funções de Data
NOW() -- data e hora atual
CURDATE() -- só data
YEAR('2024-03-15') -- 2024
MONTH('2024-03-15') -- 3
DAY('2024-03-15') -- 15
DATEDIFF('2024-12-31', '2024-01-01') -- 365
DATE_ADD(NOW(), INTERVAL 7 DAY) -- +7 dias
DATE_FORMAT(NOW(), '%d/%m/%Y') -- '15/03/2024'Funções para trabalhar com datas. O DATEDIFF retorna a diferença em dias. O DATE_ADD/DATE_SUB soma/subtrai intervalos. O DATE_FORMAT formata a saída (MySQL). Em PostgreSQL use TO_CHAR.
IF e IIF (Condicional)
-- MySQL: IF(condição, valor_true, valor_false) SELECT IF(preco > 100, 'caro', 'barato') FROM produtos; -- SQL Server: IIF SELECT IIF(stock > 0, 'disponível', 'esgotado') FROM produtos; -- Padrão SQL: CASE (funciona em todos) SELECT CASE WHEN preco > 100 THEN 'caro' ELSE 'barato' END FROM produtos;
O IF() (MySQL) e IIF() (SQL Server) são condicionais de 3 argumentos. O CASE é o padrão SQL universal e mais flexível (múltiplas condições). Prefira CASE para portabilidade.
Funções de data
-- Data/hora atual:
SELECT NOW(); -- data + hora
SELECT CURDATE(); -- só data (MySQL)
-- Extrair partes:
SELECT YEAR(data), MONTH(data), DAY(data)
FROM pedidos;
-- Aritmética de datas (MySQL):
SELECT DATE_ADD(NOW(), INTERVAL 7 DAY);
SELECT DATEDIFF('2024-12-31', NOW());
-- PostgreSQL:
SELECT NOW() + INTERVAL '7 days';
SELECT EXTRACT(YEAR FROM data);
-- Formatar (MySQL):
SELECT DATE_FORMAT(NOW(), '%d/%m/%Y');Funções de data: NOW()/CURDATE() dão a data atual; YEAR()/MONTH() extraem partes; DATE_ADD/INTERVAL fazem aritmética. A sintaxe varia entre MySQL e PostgreSQL.
Funções Numéricas
ROUND(3.14159, 2) -- 3.14 CEIL(4.1) -- 5 FLOOR(4.9) -- 4 ABS(-10) -- 10 MOD(10, 3) -- 1 POWER(2, 10) -- 1024 SQRT(144) -- 12 RAND() -- aleatório 0-1
O ROUND arredonda a N casas. O CEIL arredonda para cima, FLOOR para baixo. O MOD retorna o resto da divisão. O ROUND usa arredondamento "banker's" em alguns SGBD (0.5 → par mais próximo).
Funções de Agregação de String
-- MySQL SELECT GROUP_CONCAT(nome SEPARATOR '; ') FROM produtos GROUP BY categoria; -- PostgreSQL SELECT STRING_AGG(nome, '; ' ORDER BY nome) FROM produtos GROUP BY categoria; -- SQL Server SELECT STRING_AGG(nome, '; ') FROM produtos GROUP BY categoria;
Funções que concatenam valores de um grupo numa string. GROUP_CONCAT (MySQL), STRING_AGG (PostgreSQL/SQL Server). Aceitam separador personalizado e ordenação interna.
CONCAT e LENGTH
-- Concatenar strings:
SELECT CONCAT(nome, ' ', apelido) AS completo
FROM clientes;
-- Operador || (padrão SQL):
SELECT nome || ' ' || apelido FROM clientes;
-- Tamanho da string:
SELECT LENGTH(nome) FROM clientes;
-- Maiúsculas/minúsculas:
SELECT UPPER(nome), LOWER(email) FROM clientes;
-- Substring:
SELECT SUBSTRING(nome, 1, 3) FROM clientes;
-- TRIM remove espaços:
SELECT TRIM(' olá ');Funções de string: CONCAT() ou o operador || juntam strings; LENGTH() dá o tamanho; UPPER/LOWER mudam a capitalização; SUBSTRING() extrai partes; TRIM() remove espaços.
COALESCE e IFNULL
-- Retorna o primeiro não-NULL SELECT COALESCE(telefone, telemovel, 'sem contacto') FROM clientes; -- MySQL: IFNULL (só 2 args) SELECT IFNULL(desconto, 0) FROM produtos; -- PostgreSQL: NULLIF (inverso) SELECT NULLIF(stock, 0) FROM produtos; -- Retorna NULL se stock = 0 (evita divisão por zero)
O COALESCE retorna o primeiro valor não-NULL da lista (padrão SQL). O IFNULL é MySQL (só 2 args). O NULLIF(a, b) retorna NULL se a=b — útil para evitar divisão por zero.
Funções de Sistema e Metadata
-- Informação da sessão SELECT DATABASE(); -- BD atual SELECT USER(); -- utilizador SELECT VERSION(); -- versão do SGBD SELECT LAST_INSERT_ID(); -- último AUTO_INCREMENT -- Listar tabelas SHOW TABLES; -- MySQL \dt -- PostgreSQL (psql) -- Descrever tabela DESCRIBE clientes; -- MySQL \d clientes -- PostgreSQL (psql)
Funções de sistema retornam informação sobre a sessão e estrutura. O LAST_INSERT_ID() retorna o último ID gerado. SHOW TABLES/DESCRIBE são comandos MySQL; em PostgreSQL use \dt e \d no psql.
Dicas e Boas Práticas
Evitar SELECT *
-- Mau: traz tudo (colunas desnecessárias, mais I/O) SELECT * FROM clientes; -- Bom: só o necessário SELECT id, nome, email FROM clientes; -- Em JOINs, nunca use * SELECT c.nome, p.total FROM clientes c JOIN pedidos p ON c.id = p.cliente_id;
O SELECT * traz colunas desnecessárias, aumenta tráfego de rede e impede otimizações de índices covering. Liste sempre as colunas necessárias. Em JOINs, pode trazer colunas de tabelas erradas.
Comentários SQL
-- Comentário de linha (MySQL, PostgreSQL) # Comentário de linha (só MySQL) /* Comentário de bloco (todos os SGBD) */ -- Dica: documentar consultas complexas /* Relatório mensal: soma vendas por categoria Exclui devoluções (status != 3) */ SELECT categoria, SUM(total) FROM vendas WHERE status != 3 GROUP BY categoria;
Use -- para comentários de linha (padrão SQL), # (só MySQL) e /* */ para blocos. Documente consultas complexas com o "porquê" e não o "o quê". Útil para desativar partes temporariamente.
Paginação eficiente (keyset)
-- OFFSET grande é LENTO -- (percorre e descarta N linhas): SELECT * FROM pedidos ORDER BY id LIMIT 20 OFFSET 100000; -- Keyset pagination é RÁPIDO -- (usa o índice diretamente): SELECT * FROM pedidos WHERE id > 100000 -- último id visto ORDER BY id LIMIT 20; -- Guarda o último id da página -- e passa-o no próximo pedido
OFFSET grande obriga a percorrer e descartar milhares de linhas (lento). Keyset pagination (WHERE id > último) usa o índice diretamente — velocidade constante em qualquer página. Ideal para feeds e listagens longas.
Prevenir SQL Injection
-- NUNCA concatenar input: -- "SELECT * FROM users WHERE id = " + input ← PERIGO -- SEMPRE usar prepared statements: SELECT * FROM users WHERE id = ?; SELECT * FROM users WHERE email = ?; -- Validar e escapar no lado da aplicação -- Usar ORM (Eloquent, SQLAlchemy, etc.)
Nunca concatene input do utilizador em SQL — permite SQL injection. Use sempre prepared statements com placeholders (? ou :nome). Valide tipos no servidor. Prefira um ORM com query builder.
N+1 Queries e Performance
-- Problema N+1: 1 query + N queries SELECT * FROM clientes; -- 1 SELECT * FROM pedidos WHERE cliente_id = 1; -- N SELECT * FROM pedidos WHERE cliente_id = 2; -- N... -- Solução: JOIN (1 query) SELECT c.nome, p.total FROM clientes c LEFT JOIN pedidos p ON c.id = p.cliente_id; -- Ou: IN (2 queries) SELECT * FROM clientes; SELECT * FROM pedidos WHERE cliente_id IN (1,2,3,...);
O problema N+1 ocorre quando faz 1 consulta para a lista + N para os relacionados. Solução: use JOIN ou WHERE IN com os IDs. Em ORMs, use eager loading (with() no Eloquent).
EXPLAIN (analisar queries)
-- Ver o plano de execução: EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 42; -- O que procurar: -- type: ALL -> varre tudo (mau!) -- type: ref/eq_ref -> usa índice (bom) -- rows: estimativa de linhas lidas -- Com ANALYZE (MySQL 8+): EXPLAIN ANALYZE SELECT ...; -- mostra tempos reais -- Se type=ALL num filtro frequente, -- falta um índice nessa coluna
EXPLAIN mostra como a query será executada. type: ALL significa varrimento completo (falta índice); ref/eq_ref indicam uso de índice. EXPLAIN ANALYZE dá tempos reais. Ferramenta nº 1 de otimização.
Indexação Inteligente
-- Indexar colunas de WHERE e JOIN CREATE INDEX idx_email ON clientes(email); -- Índice composto: ordem importa CREATE INDEX idx_cat_preco ON produtos(categoria, preco); -- Funciona para: WHERE categoria = X -- Funciona para: WHERE categoria = X AND preco > Y -- NÃO funciona para: WHERE preco > Y (sozinho) -- Verificar se usa índice EXPLAIN SELECT * FROM produtos WHERE categoria = 'x';
Indexe colunas usadas em WHERE, JOIN e ORDER BY. Em índices compostos, a ordem importa: funciona para a primeira coluna (prefixo). Não indexe tudo — cada índice atrasa INSERT/UPDATE.
Tipos Corretos = Performance
-- Mau: VARCHAR para tudo
codigo VARCHAR(255) -- se é sempre 10 chars
-- Bom: tipo restritivo
codigo CHAR(10) -- tamanho fixo
preco DECIMAL(8,2) -- não FLOAT para dinheiro
ativo BOOLEAN -- não VARCHAR('sim'/'não')
data TIMESTAMP -- não VARCHAR('2024-01-15')Escolha o tipo mais restritivo: CHAR para tamanho fixo, DECIMAL para dinheiro, BOOLEAN para flags, TIMESTAMP para datas. Tipos corretos = menos espaço, índices menores, comparações mais rápidas.
Sempre WHERE no UPDATE/DELETE
-- PERIGO: afeta TODAS as linhas UPDATE produtos SET ativo = 0; DELETE FROM logs; -- SEGURO: testar antes SELECT COUNT(*) FROM produtos WHERE categoria = 'x'; UPDATE produtos SET ativo = 0 WHERE categoria = 'x'; -- Dica: usar transação para testar START TRANSACTION; DELETE FROM logs WHERE data < '2023-01-01'; -- Verificar resultado, depois: -- COMMIT; ou ROLLBACK;
Sem WHERE, o UPDATE/DELETE afeta todas as linhas. Teste sempre com SELECT primeiro. Use transações para poder reverter. Em produção, faça backups antes de operações em massa.
Convenções de Nomenclatura
-- Tabelas: plural, snake_case clientes, pedidos, categorias_produto -- Colunas: singular, snake_case nome, email, criado_em, cliente_id -- Chaves: tabela_singular_id cliente_id, produto_id -- Índices: idx_tabela_coluna idx_clientes_email, idx_pedidos_data -- Constraints: fk_, uk_, chk_ fk_pedidos_cliente, uk_clientes_email
Use snake_case e nomes descritivos. Tabelas no plural (clientes), FKs como tabela_id. Índices com prefixo idx_. Evite palavras reservadas (order, group, select). Consistência > preferência pessoal.