Banco de Dados: Select recursivo e subselect em tabelas com autorelacionamento

Questão de Entrevista
País: Irlanda Tipo: Técnica Assunto: Banco de Dados
Ramo de negócio da empresa: Financeiro (Banco) Grau de Dificuldade: Média

 

Data a tabela Employee, escreva consultas que retorne:

  1. O nome e o salário dos empregados que recebem mais do que os seus gerentes (manager) diretos, ordenando o resultado pelo salário de forma decrescente
  2. O nome dos empregados que possuem exatamente outros dois empregados com o mesmo dia e mês de aniversário.

tabela employee (empregado)

Abaixo o script para criar e inserir os dados da tabela (padrão ANSI, pode ser usado em qualquer banco de dados)

-- cria a tabela Employee (Empregado)
-- com chave primária nomeada como PK_Employee 
CREATE TABLE dbo.Employee(
  Id int NOT NULL,
  Name varchar(50) NOT NULL,
  Birthday datetime NOT NULL,
  Salary numeric(18, 2) NOT NULL,
  ManagerId int,
  CONSTRAINT PK_Employee 
  PRIMARY KEY (Id)
)

-- cria uma chave estrangeira (foreign key) 
-- relacionando o gerente (ManagerId) ao empregado (Id)
ALTER TABLE dbo.Employee  
WITH CHECK ADD CONSTRAINT FK_Employee_Employee 
FOREIGN KEY(ManagerId)
REFERENCES dbo.Employee (Id)

--insere registros na table a Employee 
INSERT INTO Employee 
       (Id,Name,Birthday,Salary,ManagerId) 
VALUES (1, 'John', '1978-02-22', 5000, null)

-- como todos os campos estão sendo usados, podemos simplificar 
-- o comando ocultado p nome das colunas na instrução INSERT
INSERT INTO Employee VALUES (2, 'Mary', '1983-08-25', 5200, 1)
INSERT INTO Employee VALUES (3, 'Paul', '1994-11-07', 3750, 2)
INSERT INTO Employee VALUES (4, 'Anna', '2001-12-18', 2450, 3)
INSERT INTO Employee VALUES (5, 'Aoifa', '1994-11-07', 4200, 3)
INSERT INTO Employee VALUES (6, 'Emily', '1995-03-05', 3200, 1)
INSERT INTO Employee VALUES (7, 'Alan', '1997-05-26', 3800, 3)
INSERT INTO Employee VALUES (8, 'Lucy', '2001-11-26', 3800, 2)
INSERT INTO Employee VALUES (9, 'Adan', '1989-08-25', 4760, null)
INSERT INTO Employee VALUES (10, 'Will', '2001-12-01', 2800, null)
INSERT INTO Employee VALUES (11, 'Sophia', '2002-04-08', 2930, 10)
INSERT INTO Employee VALUES (12, 'Emma', '1998-09-20', 2560, 10)
INSERT INTO Employee VALUES (13, 'James', '1995-08-25', 2560, 9)
INSERT INTO Employee VALUES (14, 'Charlotte', '1995-10-12', 2980, 9)
INSERT INTO Employee VALUES (15, 'Lucas', '1986-01-11', 2900, 10)

Vamos a questão 1: retornar o nome e o salário dos empregados que recebem mais do que os seus gerentes diretos, ordenando os registros pelo salário de forma decrescente.

Analisando os dados nota-se que a coluna ManagerId é uma auto-referência para a tabela Employee, ou seja, indica o registro na tabela que se refere ao gerente da pessoa em questão. Por exemplo: John (Id = 1) não tem um gerente direto, pois seu ManagerId é nulo. Já Mary (Id= 2) é subordinada a John, pois seu ManagerId é igual 1, que é o Id de John. Mary tem um salário de $5200, enquanto John tem um salário de $5000, logo Mary deve ser listada.

Uma das formas de resolver este problema é usando um subselect e comparar o salário do cada registro da tabela Employee com o salário do seu respectivo gerente. Como quem não tem gerente direto não precisa ser listado, por questões de desempenho pode-se adicionar um filtro para ignorar estas pessoas, com isto o subselect não será executado nestes casos (o engine do banco de dados sabe que uma expressão com subselect é mais cara, computacionalmente falando, que uma expressão que usa apenas um campo da própia tabela, como o operador condicional AND (E) exige que as duas expressões sejam verdadeira para ser verdadeiro, ele executa primeiro a condição e.ManagerId is not null e ignora a segunda expressão se a primeira for falsa):

select Name, Salary 
from Employee e
where e.ManagerId is not null
  and e.Salary > (select Salary 
                  from Employee e2 
                  where e2.Id = e.ManagerId)
order by e.Salary desc

Executando o comando acima:

Name Salary
Mary 5200.00
Aoifa 4200.00
Alan 3800.00
Sophia 2930.00
Lucas 2900.00

Outra forma de resolver o problema é fazer um INNER JOIN da tabela Employee com ela mesma. Esta forma, além de ter melhor performance, permite listar o nome e o salaário do gerente, o que facilita a verificação do resultado (isto está fora do escopo do exercício inicial, se o exercício for corrigido eletronicamente, devolva exatamente o que se pede. Se for uma programação em par – entrevistador e entrevistado – você pode usar mas deve destacar o que está fazendo):

select e.Name, e.Salary, e2.Name Manager, e2.Salary 
from Employee e
inner join (select Id, Name, Salary from Employee) e2 
      on e2.Id = e.ManagerId
where e.Salary > e2.Salary
order by e.Salary desc

Perceba que no exemplo acima a condição e.ManagerId is not null foi suprimida. Como foi usado um INNER JOIN, que obriga que o registro exista dos dois lados da junção, os registros com ManagerId = NULL são automaticamente descartados.

Name Salary Manager Salary
Mary 5200.00 John 5000.00
Aoifa 4200.00 Paul 3750.00
Alan 3800.00 Paul 3750.00
Sophia 2930.00 Will 2800.00
Lucas 2900.00 Will 2800.00

Agora a questão 2: listar o nome nome dos empregados que possuem exatamente outros dois empregados com o mesmo dia e mês de aniversário.

Para responder a questão vamos usar a mesma base de comando da primeira opção de resposta da questão 1, com alterações na cláusula where da subconsulta. Para comprar os dias e meses foram usadas, respectivamente, as funções DAY e MONTH (estas funções são do SQL Server, consulte a documentação do seu banco de dados para encontrar a função equivalente).

select Name
from Employee e
where (select count(*) 
       from Employee e2 
       where e2.Id <> e.Id
         and day(e2.Birthday) = day(e.Birthday) 
         and month(e2.Birthday) = month(e.Birthday)) = 2

O resultado é:

Name
Mary
Adan
James

Note que Aoifa e Paul também fazem aniversário no mesmo dia e mês (07 de novembro), mas o exercício pede que sejam listadas apenas o empregados que possuem exatamente outros dois com o mesmo dia e mês de aniversário, por isto a inclusão da condição e2.Id <> e.Id na cláusula where da subconsulta.