Prática: Consulta Multitabelas Abrangente
Prática: Consulta Multitabelas Abrangente
📋 Requisitos do Projeto
Esta seção usa um cenário de sistema de gerenciamento de pedidos de e-commerce para aplicar abrangentemente as habilidades de consulta multitabelas aprendidas anteriormente.
Estrutura das Tabelas do Banco de Dados
Usamos um banco de dados unificado de 4 tabelas:
-- Tabela de departamentos
CREATE TABLE departments (
department_id INT PRIMARY KEY,
department_name VARCHAR(50) NOT NULL,
city VARCHAR(50),
budget DECIMAL(12,2)
);
-- Tabela de funcionários
CREATE TABLE employees (
employee_id INT PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
email VARCHAR(100),
salary DECIMAL(10,2),
department_id INT,
hire_date DATE,
FOREIGN KEY (department_id) REFERENCES departments(department_id)
);
-- Tabela de pedidos
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
employee_id INT,
order_date DATE,
total_amount DECIMAL(12,2),
status VARCHAR(20),
FOREIGN KEY (employee_id) REFERENCES employees(employee_id)
);
-- Tabela de produtos
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
price DECIMAL(10,2),
stock INT,
department_id INT,
FOREIGN KEY (department_id) REFERENCES departments(department_id)
);
Dados de Teste
-- Dados de departamentos
INSERT INTO departments VALUES (1, 'Technology', 'Beijing', 500000);
INSERT INTO departments VALUES (2, 'Sales', 'Shanghai', 300000);
INSERT INTO departments VALUES (3, 'Marketing', 'Guangzhou', 200000);
INSERT INTO departments VALUES (4, 'Finance', 'Shenzhen', 150000);
-- Dados de funcionários
INSERT INTO employees VALUES (1, 'Zhang', 'Wei', 'zhangwei@co.com', 12000, 1, '2023-03-15');
INSERT INTO employees VALUES (2, 'Li', 'Na', 'lina@co.com', 9500, 1, '2023-06-20');
INSERT INTO employees VALUES (3, 'Wang', 'Qiang', 'wangqiang@co.com', 8000, 2, '2022-11-10');
INSERT INTO employees VALUES (4, 'Zhao', 'Min', 'zhaomin@co.com', 11000, 2, '2024-01-05');
INSERT INTO employees VALUES (5, 'Liu', 'Yang', 'liuyang@co.com', 7500, 3, '2024-04-18');
INSERT INTO employees VALUES (6, 'Chen', 'Jing', 'chenjing@co.com', 9000, NULL, '2025-02-28');
-- Dados de pedidos
INSERT INTO orders VALUES (1001, 101, 3, '2026-05-10', 2500.00, 'completed');
INSERT INTO orders VALUES (1002, 102, 3, '2026-05-15', 1800.00, 'completed');
INSERT INTO orders VALUES (1003, 103, 4, '2026-06-01', 3200.00, 'shipped');
INSERT INTO orders VALUES (1004, 101, 1, '2026-06-05', 950.00, 'pending');
INSERT INTO orders VALUES (1005, 104, 4, '2026-06-10', 4100.00, 'completed');
INSERT INTO orders VALUES (1006, 105, 3, '2026-06-15', 2800.00, 'cancelled');
-- Dados de produtos
INSERT INTO products VALUES (1, 'Laptop', 6999.00, 50, 1);
INSERT INTO products VALUES (2, 'Wireless Mouse', 129.00, 200, 1);
INSERT INTO products VALUES (3, 'Office Desk', 899.00, 30, 2);
INSERT INTO products VALUES (4, 'Projector', 3500.00, 15, 3);
INSERT INTO products VALUES (5, 'Printer', 2200.00, 25, 1);
🏗️ Prática de Consultas
Consulta 1: Relatório de Desempenho de Pedidos por Funcionário
Requisito: Visualizar a contagem de pedidos e o total de vendas de cada funcionário, incluindo funcionários sem pedidos.
SELECT
e.employee_id,
CONCAT(e.first_name, e.last_name) AS nome,
d.department_name AS departamento,
COUNT(o.order_id) AS qtd_pedidos,
COALESCE(SUM(o.total_amount), 0) AS total_vendas
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
LEFT JOIN orders o ON e.employee_id = o.employee_id
GROUP BY e.employee_id, e.first_name, e.last_name, d.department_name
ORDER BY total_vendas DESC;
Resultado:
employee_id | nome | departamento | qtd_pedidos | total_vendas
-----------+------------+--------------+-------------+-------------
4 | Zhao Min | Sales | 2 | 7300.00
3 | Wang Qiang | Sales | 3 | 7100.00
1 | Zhang Wei | Technology | 1 | 950.00
2 | Li Na | Technology | 0 | 0.00
5 | Liu Yang | Marketing | 0 | 0.00
6 | Chen Jing | NULL | 0 | 0.00
Explicação do Código:
LEFT JOIN departments— Junta a tabela de departamentos para obter nomes de departamentos, mantendo funcionários sem departamentoLEFT JOIN orders— Junta a tabela de pedidos, mantendo funcionários sem pedidosCOALESCE(SUM(...), 0)— Converte NULL para 0 para evitar exibir campos vaziosGROUP BY— Agrupa por funcionário, agregando contagem de pedidos e valor total
Consulta 2: Ranking de Vendas por Departamento
Requisito: Calcular o total de vendas por departamento e classificá-los.
SELECT
d.department_name AS departamento,
d.city AS cidade,
COUNT(DISTINCT e.employee_id) AS qtd_funcionarios,
COUNT(o.order_id) AS total_pedidos,
COALESCE(SUM(o.total_amount), 0) AS total_vendas,
RANK() OVER (ORDER BY COALESCE(SUM(o.total_amount), 0) DESC) AS ranking
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id
LEFT JOIN orders o ON e.employee_id = o.employee_id
GROUP BY d.department_id, d.department_name, d.city
ORDER BY total_vendas DESC;
Explicação do Código:
- Cadeia de
LEFT JOIN: departments → employees → orders COUNT(DISTINCT e.employee_id)— Deduplica a contagem de funcionários para evitar contagem duplicada por múltiplos pedidosRANK()— Função de janela que classifica por total de vendasCOALESCE— Trata departamentos sem pedidos, exibindo 0
Consulta 3: Funcionários de Alto Salário e Detalhes de Seus Pedidos
Requisito: Encontrar funcionários cujo salário está acima da média da empresa, junto com suas informações de pedidos.
SELECT
e.employee_id,
CONCAT(e.first_name, e.last_name) AS nome,
e.salary AS salario,
o.order_id,
o.order_date AS data_pedido,
o.total_amount AS valor_pedido,
o.status AS status_pedido
FROM employees e
INNER JOIN orders o ON e.employee_id = o.employee_id
WHERE e.salary > (
SELECT AVG(salary) FROM employees
)
ORDER BY e.salary DESC, o.order_date;
Explicação do Código:
- Subconsulta
(SELECT AVG(salary) FROM employees)— Calcula o salário médio da empresa INNER JOIN— Mantém apenas funcionários de alto salário que possuem pedidosWHERE e.salary > (...)— Usa o resultado da subconsulta como condição de filtroORDER BYordena por salário decrescente primeiro, depois por data crescente
Consulta 4: Consulta Cruzada — Pedidos Sem Produtos do Departamento Correspondente
Requisito: Encontrar pedidos cujo departamento do funcionário não possui produtos.
SELECT
o.order_id,
CONCAT(e.first_name, e.last_name) AS nome_funcionario,
d.department_name AS departamento,
o.total_amount AS valor_pedido
FROM orders o
INNER JOIN employees e ON o.employee_id = e.employee_id
LEFT JOIN departments d ON e.department_id = d.department_id
WHERE e.department_id IS NOT NULL
AND e.department_id NOT IN (
SELECT DISTINCT department_id FROM products WHERE department_id IS NOT NULL
);
Explicação do Código:
INNER JOIN employees— Junta pedidos e funcionáriosLEFT JOIN departments— Obtém nomes de departamentosNOT IN (subconsulta)— Exclui departamentos que possuem produtos- Combina JOIN e subconsulta para filtragem mista
Consulta 5: Usar UNION para Mesclar Dados Multidimensionais
Requisito: Gerar um diretório abrangente mesclando "funcionários com pedidos" e "todos os departamentos".
-- Funcionários com pedidos concluídos
SELECT
CONCAT(first_name, last_name) AS nome,
'Funcionário' AS tipo,
CAST(salary AS VARCHAR) AS detalhes
FROM employees
WHERE employee_id IN (
SELECT DISTINCT employee_id FROM orders WHERE status = 'completed'
)
UNION ALL
-- Todos os departamentos
SELECT
department_name,
'Departamento',
CONCAT('Orçamento: ', CAST(budget AS VARCHAR))
FROM departments
ORDER BY tipo, nome;
Explicação do Código:
- Primeiro SELECT — Usa subconsulta para filtrar funcionários com pedidos concluídos
- Segundo SELECT — Obtém todos os departamentos
UNION ALL— Mescla dois conjuntos de resultados, mantendo todas as linhas- Adiciona uma coluna constante "tipo" para distinguir fontes de dados
Consulta 6: Análise Abrangente — Distribuição de Status de Pedidos e Desempenho de Funcionários
Requisito: Calcular a distribuição de status de pedidos para cada funcionário e encontrar funcionários que processaram pedidos "concluídos".
SELECT
CONCAT(e.first_name, e.last_name) AS nome_funcionario,
d.department_name AS departamento,
SUM(CASE WHEN o.status = 'completed' THEN 1 ELSE 0 END) AS concluidos,
SUM(CASE WHEN o.status = 'shipped' THEN 1 ELSE 0 END) AS enviados,
SUM(CASE WHEN o.status = 'pending' THEN 1 ELSE 0 END) AS pendentes,
SUM(CASE WHEN o.status = 'cancelled' THEN 1 ELSE 0 END) AS cancelados,
COUNT(o.order_id) AS total_pedidos
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
LEFT JOIN orders o ON e.employee_id = o.employee_id
WHERE e.employee_id IN (
SELECT DISTINCT employee_id FROM orders
)
GROUP BY e.employee_id, e.first_name, e.last_name, d.department_name
ORDER BY total_pedidos DESC;
Explicação do Código:
CASE WHENagregação condicional — Converte dados de linhas em colunas (pivô)LEFT JOINmantém todos os dados associados- Subconsulta
IN (...)filtra funcionários sem pedidos GROUP BYagrupa e agrega por funcionário
❓ Perguntas Frequentes
P: Quando devo usar INNER JOIN vs LEFT JOIN? R: Se você precisar manter todos os registros da tabela da esquerda (mesmo quando a tabela da direita não tiver correspondência), use
LEFT JOIN. Se você só precisa de registros que correspondem em ambas as tabelas, useINNER JOIN. Na prática, consultas de relatórios tipicamente usamLEFT JOIN, enquanto validação de dados usaINNER JOIN.
P: Qual tem melhor desempenho, subconsulta ou JOIN? R: Otimizadores de bancos de dados modernos tipicamente convertem subconsultas em JOINs para execução. Para associações simples,
JOINé mais intuitivo; para verificações de existência ("existe"), subconsultasEXISTSouINsão mais semânticas. UseEXPLAINpara verificar diferenças de desempenho.
P: Como depurar consultas multitabelas complexas? R: Construa incrementalmente: comece com a subconsulta mais interna, verifique se o resultado está correto, e então envolva camada por camada. Você também pode usar CTEs (cláusula
WITH) para dividir consultas complexas em múltiplos conjuntos de resultados temporários legíveis.
P: O SELECT após GROUP BY pode conter apenas funções de agregação e colunas agrupadas? R: Sim. Colunas não agregadas em
SELECTdevem todas aparecer emGROUP BY, caso contrário o banco de dados lançará um erro. Este é um requisito do padrão SQL.
📖 Resumo
| Habilidade | Aplicação nesta Seção |
|---|---|
| INNER JOIN | Associar tabelas com relacionamentos de correspondência |
| LEFT JOIN | Manter todos os registros da tabela da esquerda |
| Subconsulta (WHERE IN) | Usada como condição de filtro |
| Subconsulta (escalar) | Retorna um valor único para comparação |
| UNION ALL | Mesclar conjuntos de resultados de diferentes dimensões |
| CASE WHEN | Agregação condicional (pivô) |
| Funções de Agregação | COUNT, SUM, AVG, COALESCE |
| Funções de Janela | RANK() para classificação |
Pontos-Chave de Prática:
- Construa consultas complexas incrementalmente do simples ao complexo
- Garanta que consultas de tabela única estejam corretas antes de adicionar junções multitabelas
- Use
COALESCEpara tratar valores NULL - Use
EXPLAINpara analisar desempenho de consultas
📝 Exercícios
- Escreva uma consulta: Encontrar as informações do departamento do funcionário com o maior salário, incluindo nome do departamento, cidade e orçamento.
- Escreva uma consulta: Contar o número de departamentos e total de funcionários por cidade, usando
LEFT JOINpara garantir que cidades sem funcionários também sejam exibidas. - Escreva uma consulta: Encontrar funcionários que nunca processaram um pedido, implementando tanto com subconsulta (
NOT IN) quanto comLEFT JOIN ... IS NULL, e comparar se os resultados são consistentes. - Escreva uma consulta: Dividir funcionários em três grupos por salário — "Alto (>10000)", "Médio (6000-10000)", "Baixo (<6000)" — e contar o número de pessoas e salário médio para cada grupo.
Próxima Lição
A seguir aprenderemos Funções de Agregação — domine COUNT, SUM, AVG, MAX, MIN, além do uso avançado de GROUP BY e HAVING.



