40 Queries SQL Avançadas

Por Réulison Silva
Réulison Silva
Published on
Primeiros passos com o SQL

Fala, aspirante a Data Analyst! Se você acha que SQL é só decorar SELECT * FROM tabela, a gente precisa conversar. O mercado não quer alguém que decore comandos, quer alguém que resolva problemas reais.

O SQL fica muito mais fácil (e divertido) quando você para de tentar decorar a sintaxe e começa a pensar em como resolver desafios do dia a dia. Pensando nisso, compilei uma lista matadora com 40 queries avançadas que cobrem os principais cenários do trabalho real.

O que você vai aprender:

  • Subqueries e Joins: O pão com manteiga de qualquer entrevista.
  • GROUP BY e HAVING: Para agregar dados e filtrar grupos.
  • Window Functions: O nível sênior do SQL.
  • Ranking e Top-N: Achar os "Top 3", "Segundo maior", etc.
  • Registros Duplicados: Como limpar a sujeira.
  • Análise de Salários e Funcionários: Clássicos de RH.
  • Análise de Datas e Meses: Tendências e sazonalidade.
  • Análise de Clientes e Pedidos: O coração do e-commerce.

Essas queries são o filtro para vagas de Data Analyst, Data Engineer e Desenvolvedor. Vou destrinchar alguns exemplos práticos para você sentir o gostinho.

Nível 1: O Básico que Diferencia

01. Como encontrar o segundo maior salário?

Lógica: Pegue o maior salário, mas que seja menor que o maior salário geral.

SELECT MAX(salary) AS second_highest_salary
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

Output:

second_highest_salary
90000

02. Funcionários que entraram no mesmo mês/ano que o gerente

Lógica: Self Join na tabela de funcionários e comparação de datas.

SELECT e.employee_id, e.name
FROM employees e
JOIN employees m ON e.manager_id = m.employee_id
WHERE MONTH(e.join_date) = MONTH(m.join_date)
AND YEAR(e.join_date) = YEAR(m.join_date);

03. Contar nomes que começam e terminam com a mesma letra

Lógica: Use LEFT e RIGHT para comparar a primeira e a última letra.

SELECT COUNT(*) AS total_employees
FROM employees
WHERE LEFT(name, 1) = RIGHT(name, 1);

04. Departamento com o maior salário médio

Lógica: Agrupe por departamento, calcule a média e ordene decrescente.

SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
ORDER BY avg_salary DESC
LIMIT 1;

Output:

department_idavg_salary
185000

Nível 2: Subqueries e Window Functions

05. Funcionários que ganham mais que a média do seu departamento

Lógica: Subquery correlacionada. A média é calculada por departamento.

SELECT e.employee_id, e.name, e.salary, e.department_id
FROM employees e
WHERE e.salary > (SELECT AVG(salary)
                  FROM employees
                  WHERE department_id = e.department_id);

06. Top 3 funcionários mais bem pagos

Lógica: Ordenação simples e limite.

SELECT employee_id, name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3;

07. Encontrar e-mails duplicados

Lógica: Agrupe por e-mail e conte. Se a contagem for maior que 1, é duplicado.

SELECT email, COUNT(*) AS total_count
FROM employees
GROUP BY email
HAVING COUNT(*) > 1;

08. Salário acumulado ordenado por salário

Lógica: Window Function SUM() OVER().

SELECT employee_id, name, salary,
       SUM(salary) OVER (ORDER BY salary) AS total_salary
FROM employees
ORDER BY salary;

Nível 3: Análise de Departamentos e Duplicatas

09. Funcionários que ganham mais que a média do seu departamento

Lógica: Similar à query 05, mas com foco no ID do departamento.

SELECT e.employee_id, e.name, e.salary, e.department_id
FROM employees e
WHERE e.salary > (
    SELECT AVG(salary)
    FROM employees
    WHERE department_id = e.department_id
);
employee_idnamesalarydepartment_id
101John900001
105Alice750002
109Robert850001

10. Encontrar registros duplicados baseados em uma coluna

Lógica: GROUP BY + HAVING.

SELECT email, COUNT(*) AS count
FROM employees
GROUP BY email
HAVING COUNT(*) > 1;

Output:

emailcount
john@example.com2
alice@example.com2

11. Top 3 funcionários mais bem pagos por departamento

Lógica: Use ROW_NUMBER() com PARTITION BY.

SELECT employee_id, name, salary, department_id
FROM (
    SELECT e.*,
           ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
    FROM employees e
) t
WHERE rn <= 3
ORDER BY department_id, salary DESC;

Output:

employee_idnamesalarydepartment_id
101John900001
109Robert850001
110Michael700001
105Alice750002
107Sophia650002
111David600002
Linhas 3-5: A Subquery (O código que está dentro dos parênteses)

Esta parte cria uma tabela temporária (que o código apelidou de t). Ela faz o seguinte:

  • ROW_NUMBER() OVER (...): Esta é um window function. Ela cria uma numeração de linhas (1, 2, 3...) baseada em uma regra.
  • PARTITION BY department_id: Diz para o banco de dados "separar" os funcionários em grupos por departamento. A contagem de linhas vai recomeçar do 1 para cada departamento diferente.
  • ORDER BY salary DESC: Dentro de cada departamento, os funcionários são ordenados do maior para o menor salário.
  • AS rn: Dá o nome de rn (abreviação de Row Number) para essa coluna de numeração. O funcionário com o maior salário do departamento ganha o rn = 1, o segundo maior rn = 2, e assim por diante.
A Query Principal (O código de fora)

Agora que cada funcionário tem um "ranking" de salário dentro do seu departamento, a parte de fora filtra o resultado:

  • WHERE rn <= 3: Filtra para trazer apenas as linhas onde o ranking é menor ou igual a 3. Ou seja, apenas o top 3 de cada departamento.
  • SELECT ...: Esconde a coluna de ranking (rn) e mostra apenas as informações limpas do funcionário: ID, nome, salário e departamento.
  • ORDER BY department_id, salary DESC: Organiza o resultado final na sua tela, agrupando por departamento e mostrando os maiores salários primeiro.

12. Departamentos com mais de 5 funcionários

Lógica: GROUP BY + HAVING.

SELECT department_id, COUNT(*) AS total_employees
FROM employees
GROUP BY department_id
HAVING COUNT(*) > 5;

Output:

department_idtotal_employees
18
26
37

Nível 4: Joins e Filtros Específicos

13. Funcionários que não pertencem a nenhum departamento

Lógica: LEFT JOIN e checar se o ID do departamento é NULL.

SELECT e.employee_id, e.name
FROM employees e
LEFT JOIN departments d
ON e.department_id = d.department_id
WHERE d.department_id IS NULL;

14. Salário total pago por cada departamento

Lógica: SUM com GROUP BY.

SELECT d.department_id, d.department_name,
       SUM(e.salary) AS total_salary
FROM departments d
LEFT JOIN employees e
ON d.department_id = e.department_id
GROUP BY d.department_id, d.department_name;

Output:

department_iddepartment_nametotal_salary
1Development240000
2HR140000
3Sales215000
4Support125000
Linhas 3-5: As tabelas e o relacionamento
  • departments d e employees e: O código define apelidos (d e e) para as tabelas de departamentos e funcionários, facilitando a escrita.
  • LEFT JOIN: Esta é a parte mais importante do relacionamento. O LEFT JOIN garante que todos os departamentos apareçam no resultado, mesmo que não haja nenhum funcionário cadastrado neles. Se um departamento estiver vazio, ele ainda será listado.
  • ON d.department_id = e.department_id: É a regra de conexão. O banco de dados cruza as tabelas onde o código do departamento for igual nas duas.
Linha 6: O agrupamento
  • O GROUP BY junta as linhas de funcionários que pertencem ao mesmo departamento. Em vez de ver uma lista com cada funcionário individual, o banco de dados "esmaga" os dados para criar uma única linha por departamento.
Linhas 1-2: O cálculo e a exibição
  • SUM(e.salary) AS total_salary: Como os dados foram agrupados, esta função de agregação (SUM) soma o salário de todos os funcionários daquele grupo. O resultado ganha o nome de total_salary. Se um departamento não tiver funcionários (por causa do LEFT JOIN), o resultado dessa soma será NULL (ou zero, dependendo do banco).
  • As outras colunas apenas exibem o ID e o nome do departamento correspondente.

15. Funcionários que ganham mais que a média da empresa

Lógica: Subquery simples.

SELECT employee_id, name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

Output:

employee_idnamesalary
101John90000
103Robert85000
105Alice75000
106Michael70000

16. Contagem de funcionários por mês de entrada

Lógica: DATE_FORMAT e GROUP BY.

SELECT DATE_FORMAT(join_date, '%Y-%m') AS join_month,
       COUNT(*) AS total_employees
FROM employees
GROUP BY join_month
ORDER BY join_month;

Output:

join_monthtotal_employees
2018-062
2019-043
2020-012
2020-031

Ou se preferir exibir o nome do mês:

SELECT DATE_FORMAT(join_date, '%M/%Y') AS join_month

Output: "January/2026" (se o banco estiver em inglês)

Nível 5: Análise de Vendas e Clientes

17. Clientes que fizeram pedidos mas nunca devolveram

Lógica: EXISTS e NOT EXISTS.

SELECT c.customer_id, c.customer_name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o
              WHERE o.customer_id = c.customer_id)
AND NOT EXISTS (SELECT 1 FROM returns r
                WHERE r.customer_id = c.customer_id);

Output:

customer_idcustomer_name
101John Doe
103Michael Johnson
107Chris Martin
110Olivia Brown
Linhas 1-2: A consulta principal
  • O banco de dados vai buscar o ID e o nome de todos os clientes da tabela customers (apelidada de c), mas eles precisam passar por dois filtros lógicos no WHERE.
Linhas 3-4: Já comprou?
  • O operador EXISTS checa se a subquery interna retorna alguma linha.
  • Para cada cliente da tabela principal, o banco olha a tabela de pedidos (orders o). Se encontrar pelo menos um pedido associado ao ID desse cliente (o.customer_id = c.customer_id), a condição é verdadeira.
  • Nota: O SELECT 1 é apenas uma convenção de performance. Não importa o dado retornado, apenas se a linha existe ou não.
Linhas 5-6: Nunca devolveu?
  • O NOT EXISTS funciona ao contrário: ele garante que a subquery interna seja vazia.
  • O banco olha para a tabela de devoluções (returns r). Se ele encontrar qualquer registro de devolução ligado ao cliente (r.customer_id = c.customer_id), esse cliente é descartado do resultado.

18. Crescimento mês a mês (MoM) das vendas

Lógica: LAG() para pegar o valor do mês anterior.

SELECT sale_month,
       total_sales,
       LAG(total_sales) OVER (ORDER BY sale_month) AS prev_month_sales,
       ROUND(((total_sales - LAG(total_sales) OVER (ORDER BY sale_month))
       / LAG(total_sales) OVER (ORDER BY sale_month)) * 100, 2) AS growth_pct
FROM monthly_sales;

Output:

sale_monthtotal_salesprev_month_salesgrowth_pct
2024-01120000NULLNULL
2024-0215000012000025.00
2024-0318000015000020.00
2024-0421000018000016.67
Linha 6: A origem dos dados
  • A query busca as informações diretamente de uma tabela chamada monthly_sales, que já possui o mês (sale_month) e o valor total vendido naquele período (total_sales).
Linha 3: Buscando o valor do mês anterior
  • LAG(): É um window function utilizada para "olhar para trás". Ela captura o valor de uma coluna da linha anterior.
  • OVER (ORDER BY sale_month): Garante que o banco de dados organize as linhas em ordem cronológica antes de olhar para trás. Assim, o LAG vai pegar exatamente a venda do mês anterior e exibi-la na coluna prev_month_sales.
Linhas 4-5: Calculando a porcentagem de crescimento

Esta fórmula calcula a variação percentual. A lógica matemática pura é:

Venda Atual−Venda AnteriorVenda Anterior×100\frac{\text{Venda Atual} - \text{Venda Anterior}}{\text{Venda Anterior}} \times 100

No SQL, ela foi escrita repetindo a função LAG:

ROUND(((total_sales - LAG(total_sales) OVER (ORDER BY sale_month))
/ LAG(total_sales) OVER (ORDER BY sale_month)) * 100, 2) AS growth_pct
  • Parte de cima da fração: total_sales - LAG(...) calcula a diferença em dinheiro entre os dois meses (se o resultado for positivo, houve lucro; se for negativo, houve queda).
  • Divisão: Divide essa diferença pelo valor do mês anterior / LAG(...) para encontrar a proporção do crescimento.
  • * 100: Transforma esse número decimal em uma porcentagem (ex: 0.15 vira 15.0).
  • ROUND(..., 2): Arredonda o resultado final para exibição com apenas duas casas decimais (ex: 15.33).
Detalhe importante

Na primeira linha do resultado (o primeiro mês histórico da tabela), a coluna prev_month_sales e a coluna growth_pct retornarão o valor NULL (vazio), pois não existe um mês anterior no banco de dados para ser comparado.

19. Top 3 produtos por receita em cada categoria

Lógica: ROW_NUMBER() com PARTITION BY.

SELECT * FROM (
    SELECT category_id, product_id, product_name, total_revenue,
           ROW_NUMBER() OVER (PARTITION BY category_id
                              ORDER BY total_revenue DESC) AS rn
    FROM product_revenue
) t
WHERE rn <= 3
ORDER BY category_id, total_revenue DESC;

Output:

category_idproduct_idproduct_nametotal_revenue
1101Laptop250000
1105Monitor180000
1110Keyboard120000
3205Shirt95000
3203Jeans85000
3207Shoes70000
Linhas 3-4: O "Ranking" Interno

O código dentro dos parênteses cria uma numeração automática para os produtos da tabela product_revenue:

  • PARTITION BY category_id: O banco "separa" os produtos em grupos por categoria. A contagem recomeça do 1 sempre que muda a categoria.
  • ORDER BY total_revenue DESC: Dentro de cada categoria, os produtos com o maior faturamento ficam no topo da lista.
  • AS rn: O produto mais lucrativo de cada categoria ganha rn = 1, o segundo ganha rn = 2, e assim por diante.
Linhas 7-8: O Filtro Externo

Do lado de fora, a consulta final utiliza o apelido t para essa tabela temporária e filtra os dados:

  • WHERE rn <= 3: Garante que apenas os 3 primeiros colocados de cada categoria sobrevivam ao filtro.
  • ORDER BY category_id, total_revenue DESC: Organiza tudo na sua tela para que os dados fiquem agrupados por categoria, mostrando os produtos mais lucrativos primeiro.

20. Funcionários com salário acima da média do seu departamento

Lógica: Subquery correlacionada.

SELECT e.employee_id, e.name, e.salary, e.department_id
FROM employees e
WHERE e.salary > (
    SELECT AVG(salary) FROM employees
    WHERE department_id = e.department_id
);

Output:

employee_idnamesalarydepartment_id
102Alice850001
104David900001
109Sophia750002
112James880003

Nível 6: Clientes e Pedidos

21. Clientes que gastaram mais de 1000 no total

SELECT c.customer_id, c.customer_name,
       SUM(o.amount) AS total_spent
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
HAVING SUM(o.amount) > 1000;

Output:

customer_idcustomer_nametotal_spent
101John Doe1200
103Michael Johnson2300
107Chris Martin1550
110Olivia Brown2250

22. Pedidos que ainda não foram enviados

SELECT order_id, customer_id, order_date, status
FROM orders
WHERE status = 'Pending';

Output:

order_idcustomer_idorder_datestatus
10021012024-05-12Pending
10051032024-05-18Pending
10081072024-05-21Pending
10111102024-05-25Pending

23. Número de pedidos por cliente

SELECT c.customer_id, c.customer_name,
       COUNT(o.order_id) AS total_orders
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY total_orders DESC;

Output:

customer_idcustomer_nametotal_orders
103Michael Johnson7
101John Doe5
110Olivia Brown4
107Chris Martin3
104Emily Davis2

24. Data do último pedido de cada cliente

SELECT c.customer_id, c.customer_name,
       MAX(o.order_date) AS latest_order_date
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY latest_order_date DESC;

Output:

customer_idcustomer_namelatest_order_date
103Michael Johnson2024-05-24
110Olivia Brown2024-05-25
101John Doe2024-05-22
107Chris Martin2024-05-21
104Emily Davis2024-05-19

Nível 7: RH e Departamentos

25. Maior salário em cada departamento

SELECT d.department_id, d.department_name,
       MAX(e.salary) AS highest_salary
FROM departments d
JOIN employees e ON d.department_id = e.department_id
GROUP BY d.department_id, d.department_name
ORDER BY highest_salary DESC;

Output:

department_iddepartment_namehighest_salary
1Development95000
3Sales90000
2HR65000
4Support45000

26. E-mails duplicados na tabela de funcionários

SELECT email, COUNT(*) AS occurrence
FROM employees
GROUP BY email
HAVING COUNT(*) > 1;

Output:

emailoccurrence
john.doe@gmail.com2
alice.work@gmail.com3
mike123@gmail.com2
emma.dev@gmail.com2

27. Segundo maior salário da tabela

SELECT MAX(salary) AS second_highest_salary
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

Output:

second_highest_salary
90000

28. Funcionários que gerenciam mais de 5 pessoas

SELECT m.employee_id, m.name,
       COUNT(e.employee_id) AS team_size
FROM employees m
LEFT JOIN employees e ON m.employee_id = e.manager_id
GROUP BY m.employee_id, m.name
HAVING COUNT(e.employee_id) > 5;

Output:

employee_idnameteam_size
101John Doe8
103Michael Johnson6
107Chris Martin7
110Olivia Brown9

Nível 8: Produtos e Vendas

29. Produtos que nunca foram pedidos

SELECT p.product_id, p.product_name
FROM products p
LEFT JOIN orders o
ON p.product_id = o.product_id
WHERE o.order_id IS NULL;

Output:

product_idproduct_name
104Tablet
105Smartwatch
106Headphones

30. Valor total de vendas por produto

SELECT p.product_name,
       SUM(o.amount) AS total_sales
FROM products p
JOIN orders o ON p.product_id = o.product_id
GROUP BY p.product_name
ORDER BY total_sales DESC;

Output:

product_nametotal_sales
Laptop135000
Smartphone95000
Keyboard45000
Mouse15000

31. Clientes que fizeram mais de 3 pedidos

SELECT c.customer_id, c.customer_name,
       COUNT(o.order_id) AS total_orders
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
HAVING COUNT(o.order_id) > 3;

Output:

customer_idcustomer_nametotal_orders
101John Doe5
103Michael Johnson6
107Chris Martin4
110Olivia Brown7
Linhas 3-4: Cruzando Clientes e Pedidos
  • JOIN (ou INNER JOIN): Junta a tabela de clientes (customers) com a tabela de pedidos (orders). Ao contrário do LEFT JOIN que vimos antes, o INNER JOIN garante que apenas clientes que possuem pedidos entrem no resultado. Clientes que nunca compraram nada ficam de fora.
Linha 2 e 5: Agrupando e Contando
  • O GROUP BY junta todas as linhas de pedidos de um mesmo cliente em uma única linha de resultado.
  • COUNT(o.order_id) AS total_orders (no SELECT): Conta quantos IDs de pedidos existem para cada cliente agrupado, salvando esse número na coluna total_orders.
Linha 6: O Filtro do Grupo
  • HAVING: Funciona exatamente como um WHERE, mas com uma diferença crucial: o WHERE filtra linhas individuais antes do agrupamento, enquanto o HAVING filtra o resultado depois que o agrupamento e as contagens já foram feitos.
  • Neste caso, ele descarta da tela qualquer cliente que tenha feito 1, 2 ou 3 pedidos, deixando apenas quem fez 4 ou mais.

32. Média de valor dos pedidos por cliente

SELECT c.customer_id, c.customer_name,
       ROUND(AVG(o.amount), 2) AS avg_order_amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY avg_order_amount DESC;

Output:

customer_idcustomer_nameavg_order_amount
103Michael Johnson31500.75
110Olivia Brown28500.50
101John Doe22600.33
107Chris Martin18750.25

Nível 9: Desafios de Categoria

33. Categoria com o maior preço médio de produto

SELECT category_id, AVG(price) AS avg_price
FROM products
GROUP BY category_id
ORDER BY avg_price DESC
LIMIT 1;

Output:

category_idavg_price
212500.00

34. Clientes que não fizeram nenhum pedido

SELECT c.customer_id, c.customer_name
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;

Output:

customer_idcustomer_name
104Emily Davis
111Daniel Wilson
115Sophia Taylor

35. Top 2 clientes por valor total gasto

SELECT c.customer_id, c.customer_name,
       SUM(o.amount) AS total_spent
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY total_spent DESC
LIMIT 2;

Output:

customer_idcustomer_nametotal_spent
101John Doe48500
103Michael Johnson36200

36. Pedidos com valor maior que a média geral

SELECT order_id, customer_id, order_date, amount
FROM orders
WHERE amount > (SELECT AVG(amount) FROM orders);

Output:

order_idcustomer_idorder_dateamount
10031012024-05-102500
10051032024-05-183200
10071072024-05-214100
10101102024-05-252800

Nível 10: Análise Temporal e Avançada

37. Meses com mais de 10 pedidos

SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,
       COUNT(*) AS total_orders
FROM orders
GROUP BY month
HAVING COUNT(*) > 10
ORDER BY month;

Output:

monthtotal_orders
2024-0412
2024-0518
2024-0615
2024-0711

38. Funcionários que ganham mais que a média salarial

SELECT employee_id, name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

Output:

employee_idnamesalary
101John Doe75000
103Michael Johnson65000
105Olivia Brown70000
107Chris Martin62000

39. Categorias que não têm produtos

SELECT c.category_id, c.category_name
FROM categories c
LEFT JOIN products p
ON c.category_id = p.category_id
WHERE p.product_id IS NULL;

Output:

category_idcategory_name
5Toys
6Books
7Beauty

40. Total acumulado de vendas por data de pedido

SELECT order_date,
       SUM(amount) AS daily_total,
       SUM(SUM(amount)) OVER (ORDER BY order_date) AS running_total
FROM orders
GROUP BY order_date
ORDER BY order_date;

Output:

order_datedaily_totalrunning_total
2024-05-1025002500
2024-05-1132005700
2024-05-1241009800
2024-05-13280012600

Conclusão

Dominar essas 40 queries te coloca em uma posição de destaque. O segredo é praticar. Crie um banco de dados de testes e rode cada uma delas. Entenda a lógica por trás de cada JOIN, GROUP BY e OVER().

A jornada para se tornar um especialista em dados começa com uma base sólida de SQL. Bora codar! 💻

Fique ligado

Seja um Expert em Growth

Receba insights práticos sobre marketing, dados, performance e tecnologia direto no seu email.