Database
 sql >> Base de Dados >  >> RDS >> Database

Seu guia definitivo para junções SQL:OUTER JOIN – Parte 2


A junção externa está no centro do palco hoje. E esta é a parte 2 do seu guia definitivo para junções SQL. Se você perdeu a parte 1, aqui está o link.

Ao que parece, exterior é o oposto de interior. No entanto, se você considerar a junção externa dessa maneira, ficará confuso. Além disso, você não precisa incluir a palavra exterior em sua sintaxe explicitamente. É opcional!

Mas antes de nos aprofundarmos, vamos discutir nulos sobre junções externas.

Nulos e OUTER JOIN


Quando você junta 2 tabelas, um dos valores de qualquer tabela pode ser nulo. Para INNER JOINs, os registros com nulos não corresponderão e serão descartados e não aparecerão no conjunto de resultados. Se você deseja obter os registros que não correspondem, sua única opção é OUTER JOIN.

Voltando aos antônimos, não é o oposto de INNER JOINs? Não inteiramente, como você verá na próxima seção.

Tudo sobre SQL Server OUTER JOIN


A compreensão das associações externas começa com a saída. Aqui está uma lista completa do que você pode esperar:
  • Todos os registros que correspondem à condição ou predicado de junção. Essa é a expressão logo após a palavra-chave ON, muito parecida com a saída INNER JOIN. Chamamos esse problema de linha interna .
  • Valores não NULL da esquerda tabela com as contrapartes nulas da direita tabela. Chamamos esse problema de linhas externas .
  • Valores não NULL da direita tabela com as contrapartes nulas da esquerda tabela. Esta é outra forma de linhas externas.
  • Por fim, pode ser uma combinação de todas as coisas descritas acima.

Com essa lista, podemos dizer que OUTER JOIN retorna linhas internas e externas .
  • Inner – porque os resultados exatos do INNER JOIN podem ser devolvido.
  • Outer – porque as linhas externas também podem ser devolvido.

É a diferença do INNER JOIN.

INNER JOIN RETORNA SOMENTE LINHAS INTERNAS. OUTER JOINS PODEM RETORNAR LINHAS INTERNAS E EXTERNAS

Observe que eu usei “pode ser” e “também pode ser”. Depende de sua cláusula WHERE (ou se você incluir uma cláusula WHERE) se ela retornar linhas internas e/ou externas.

Mas a partir de uma instrução SELECT, como você pode determinar qual é a tabela esquerda ou direita ? Boa pergunta!

Como saber qual é a tabela esquerda ou direita em uma junção?


Podemos responder a esta pergunta com exemplos:
SELECT *
FROM Table1 a
LEFT OUTER JOIN Table2 b on a.column1 = b.column1

Do exemplo acima, Tabela1 é a tabela à esquerda e Tabela2 é a mesa certa. Agora, vamos a outro exemplo. Desta vez, é uma simples junção múltipla.
SELECT *
FROM Table1 a
LEFT OUTER JOIN Table2 b on a.column1 = b.column1
LEFT OUTER JOIN Table3 c on b.column2 = c.column1

Neste caso, para saber o que está à esquerda ou à direita, lembre-se que uma junção funciona em 2 tabelas.

Tabela1 ainda é a tabela da esquerda e Tabela2 é a mesa certa. Isso se refere à junção de 2 tabelas:Tabela1 e Tabela2 . Que tal juntar-se à Tabela2 e Tabela3 ? Tabela 2 torna-se a tabela da esquerda e Tabela3 é a mesa certa.

Se adicionarmos uma quarta tabela, Tabela3 torna-se a tabela da esquerda e a Tabela4 é a mesa certa. Mas não termina aí. Podemos juntar outra tabela à Tabela1 . Aqui está um exemplo:
SELECT *
FROM Table1 a
LEFT OUTER JOIN Table2 b on a.column1 = b.column1
LEFT OUTER JOIN Table3 c on b.column2 = c.column1
LEFT OUTER JOIN Table4 d on c.column1 = d.column2
LEFT OUTER JOIN Table5 e on a.column2 = e.column1

Tabela1 é a tabela à esquerda e Tabela5 é a mesa certa. Você também pode fazer o mesmo com as outras tabelas.

Ok, vamos voltar para a lista de saídas esperadas acima. Também podemos derivar os tipos de junções externas a partir deles.

Tipos de junções externas


Existem 3 tipos baseados nas saídas OUTER JOIN.

LEFT OUTER JOIN (LEFT JOIN)


LEFT JOIN retorna linhas internas + valores não NULL da esquerda table com as contrapartes nulas da tabela correta. Portanto, é LEFT JOIN porque a tabela da esquerda é a dominante das duas tabelas dentro da junção com valores não nulos.
LEFT OUTER JOIN EXEMPLO 1
-- Return all customerIDs with orders and no orders

USE AdventureWorks
GO

SELECT
 c.CustomerID
,soh.OrderDate
FROM Sales.Customer c
LEFT OUTER JOIN Sales.SalesOrderHeader soh ON c.CustomerID = soh.CustomerID 

No exemplo acima, o Cliente é a tabela à esquerda e SalesOrderHeader é a mesa certa. O resultado da consulta é 32.166 registros – inclui linhas internas e externas. Você pode ver uma parte dela na Figura 1:

Suponha que queremos retornar apenas as linhas externas ou os clientes sem pedidos. Para fazer isso, adicione uma cláusula WHERE para incluir apenas linhas com nulos de SalesOrderHeader .
SELECT
 c.CustomerID
,soh.OrderDate
FROM Sales.Customer c
LEFT OUTER JOIN Sales.SalesOrderHeader soh ON c.CustomerID = soh.CustomerID
WHERE soh.SalesOrderID IS NULL

O conjunto de resultados que obtive é 701 registros . Todos eles gostam do nulo OrderDate da Figura 1.

Se eu obtiver apenas as linhas internas, o resultado será 31.465 registros . Eu posso fazer isso alterando a cláusula WHERE para incluir esses SalesOrderIDs que não são nulos. Ou posso alterar a junção para um INNER JOIN e remover a cláusula WHERE.

Para ver se verifica a saída do primeiro exemplo sem a cláusula WHERE, vamos resumir os registros.
Linhas internas Linhas externas Total de linhas
31.465 registros 701 registros 32.166 registros

A partir do Total de Linhas acima com 32.166 registros, você pode ver que verifica com os primeiros resultados de exemplo. Isso também mostra como LEFT OUTER JOIN funciona.
LEFT OUTER JOIN EXEMPLO 2

Desta vez, o exemplo é uma junção múltipla. Observe também que eliminamos a palavra-chave OUTER.
-- show the people with and without addresses from AdventureWorks
USE AdventureWorks
GO

SELECT
 P.FirstName
,P.MiddleName
,P.LastName
,a.AddressLine1
,a.AddressLine2
,a.City
,adt.Name AS AddressType
FROM Person.Person p
LEFT JOIN Person.BusinessEntityAddress bea ON P.BusinessEntityID = bea.BusinessEntityID
LEFT JOIN Person.Address a ON bea.AddressID = a.AddressID
LEFT JOIN person.AddressType adt ON bea.AddressTypeID = adt.AddressTypeID 

Gerou 19.996 registros. Você pode conferir a parte da saída na Figura 2 abaixo. Os registros com AddressLine1 nulo são linhas externas. Acima dela estão as linhas internas.

RIGHT OUTER JOIN (RIGHT JOIN)


RIGHT JOIN retorna linhas internas + valores não NULL da direita table com as contrapartes nulas da tabela à esquerda.
EXEMPLO DE JUNÇÃO EXTERNA DIREITA 1
-- From the product reviews, return the products without product reviews
USE AdventureWorks
GO

SELECT
P.Name
FROM Production.ProductReview pr
RIGHT OUTER JOIN Production.Product p ON pr.ProductID = p.ProductID
WHERE pr.ProductReviewID IS NULL 

A Figura 3 mostra 10 de 501 registros no conjunto de resultados.

No exemplo acima, ProductReview é a tabela à esquerda e o Produto é a mesa certa. Como este é um RIGHT OUTER JOIN, pretendemos incluir os valores Non-NULL da tabela direita.

No entanto, escolher entre LEFT JOIN ou RIGHT JOIN depende de você. Por quê? Porque você pode expressar a consulta, seja LEFT ou RIGHT JOIN, e obter os mesmos resultados. Vamos tentar com um LEFT JOIN.
-- return the products without product reviews using LEFT OUTER JOIN
USE AdventureWorks
GO

SELECT
P.Name
FROM Production.Product p
LEFT OUTER JOIN Production.ProductReview pr ON pr.ProductID = p.ProductID
WHERE pr.ProductReviewID IS NULL

Tente executar o acima e você terá o mesmo resultado da Figura 3. Mas você acha que o Query Optimizer irá tratá-los de forma diferente? Vamos descobrir no Plano de Execução de ambos na Figura 4.

Se você é novo nisso, há algumas surpresas no Plano de Execução.
  1. Os diagramas têm a mesma aparência e são:tente um Comparar plano de exibição , e você verá o mesmo QueryPlanHash .
  2. Observe o diagrama superior com uma junção Merge. Usamos um RIGHT OUTER JOIN, mas o SQL Server o alterou para LEFT OUTER JOIN. Também trocou as tabelas da esquerda e da direita. Isso o torna igual à segunda consulta com LEFT JOIN.

Como você vê agora, os resultados são os mesmos. Então, escolha qual dos OUTER JOINs será mais conveniente.
Por que o SQL Server alterou o RIGHT JOIN para LEFT JOIN?

O mecanismo de banco de dados não precisa seguir a maneira como você expressa as junções lógicas. Enquanto puder produzir resultados corretos da maneira mais rápida que achar possível, fará mudanças. Até atalhos.

Não conclua que RGHT JOIN é ruim e LEFT JOIN é bom.
EXEMPLO DE JUNÇÃO EXTERNA DIREITA 2

Dê uma olhada no exemplo abaixo:
-- Get the unassigned addresses and the address types with no addresses
SELECT
 P.FirstName
,P.MiddleName
,P.LastName
,a.AddressLine1
,a.AddressLine2
,a.City
,adt.Name AS AddressType
FROM Person.Person p
RIGHT JOIN Person.BusinessEntityAddress bea ON P.BusinessEntityID = bea.BusinessEntityID
RIGHT JOIN Person.Address a ON bea.AddressID = a.AddressID
RIGHT JOIN person.AddressType adt ON bea.AddressTypeID = adt.AddressTypeID
WHERE P.BusinessEntityID IS NULL 

Há 2 coisas que você pode obter dessa consulta, como você pode ver na Figura 5 abaixo:

Os resultados da consulta mostram o seguinte:
  1. Os endereços não atribuídos – esses registros são aqueles com nomes nulos.
  2. Tipos de endereço sem endereços. Os tipos de endereço Arquivo, Faturamento e Primário não têm endereços correspondentes. Esses são dos registros 817 a 819.

FULL OUTER JOIN (FULL JOIN)


FULL JOIN retorna uma combinação de linhas internas e externas, esquerda e direita.
-- Get people with and without addresses, unassigned addresses, and address types without addresses
SELECT
 P.FirstName
,P.MiddleName
,P.LastName
,a.AddressLine1
,a.AddressLine2
,a.City
,adt.Name AS AddressType
FROM Person.Person p
FULL JOIN Person.BusinessEntityAddress bea ON P.BusinessEntityID = bea.BusinessEntityID
FULL JOIN Person.Address a ON bea.AddressID = a.AddressID
FULL JOIN person.AddressType adt ON bea.AddressTypeID = adt.AddressTypeID

O conjunto de resultados inclui 20.815 registros. Como o esperado, é um número total de registros do conjunto de resultados de INNER JOIN, LEFT JOIN e RIGHT JOIN.

LEFT e RIGHT JOIN incluem uma cláusula WHERE para mostrar apenas os resultados com nulos nas tabelas esquerda ou direita.
INNER JOIN LEFT JOIN
(ONDE a.AddressID É NULO)
JUNÇÃO DIREITA
(ONDE P.BusinessEntityID É NULO)
TOTAL (O mesmo que FULL JOIN)
18.798 registros 1.198 registros 819 registros 20.815 registros

Observe que o FULL JOIN pode produzir um enorme conjunto de resultados de tabelas grandes. Portanto, use-o apenas quando precisar.

Usos práticos de OUTER JOIN


Se você ainda hesita quando pode e deve usar o OUTER JOIN, aqui estão algumas ideias.

Juntas externas que geram linhas internas e externas


Exemplos podem ser:
  • Lista alfabética de pedidos de clientes pagos e não pagos.
  • Lista alfabética de funcionários com atraso ou sem registro de atraso.
  • Uma lista de segurados que renovaram e não renovaram suas apólices de seguro mais recentes.

Juntas externas que produzem somente linhas externas


Exemplos incluem:
  • lista alfabética de funcionários sem registro de atraso para o prêmio de atraso zero
  • lista de territórios sem clientes
  • lista de agentes de vendas sem vendas de um determinado produto
  • obtendo resultados de valores ausentes, como datas sem pedidos de venda em um determinado período (exemplo abaixo)
  • nós sem filho em um relacionamento pai-filho (exemplo abaixo)

Como obter resultados de valores ausentes


Suponha que você precise produzir um relatório. Esse relatório deve mostrar o número de dias de cada mês em um determinado período em que não houve pedidos. O SalesOrderHeader em AdventureWorks contém as OrderDates , mas eles não têm datas sem pedidos. O que você pode fazer?
1. Crie uma tabela de todas as datas em um período

Um exemplo de script abaixo criará uma tabela de datas para todo o ano de 2014:
DECLARE @StartDate date = '20140101', @EndDate date = '20141231';

CREATE TABLE dbo.Dates
(
	d DATE NOT null PRIMARY KEY
)

WHILE @StartDate <= @EndDate
BEGIN
  INSERT Dates([d]) SELECT @StartDate;
  SET @StartDate = DATEADD(DAY, 1, @StartDate);
END

SELECT d FROM Dates ORDER BY [d];
2. Use LEFT JOIN para gerar os dias sem pedidos
SELECT
 MONTH(d.d) AS [month]
,YEAR(d.d) AS [year]
,COUNT(*) AS NoOrderDays
FROM Dates d
LEFT JOIN Sales.SalesOrderHeader soh ON d.d = soh.OrderDate
WHERE soh.OrderDate IS NULL
GROUP BY YEAR(d.d), MONTH(d.d)
ORDER BY [year], [month]

O código acima conta o número de dias em que nenhum pedido foi feito. SalesOrderHeader contém as datas com pedidos. Portanto, os nulos retornados na junção contarão como dias sem pedidos.

Enquanto isso, se você quiser saber as datas exatas, poderá remover a contagem e o agrupamento.
SELECT
 d.d
,soh.OrderDate
FROM Dates d
LEFT JOIN Sales.SalesOrderHeader soh ON d.d = soh.OrderDate
WHERE soh.OrderDate IS NULL

Ou, se você quiser contar os pedidos em um determinado período e ver qual data tem zero pedidos, veja como:
SELECT DISTINCT
 D.d AS SalesDate
,COUNT(soh.OrderDate) AS NoOfOrders
FROM Dates d
LEFT JOIN Sales.SalesOrderHeader soh ON d.d = soh.OrderDate
WHERE d.d BETWEEN '02/01/2014' AND '02/28/2014'
GROUP BY d.d
ORDER BY d.d

O código acima conta os pedidos de fevereiro de 2014. Veja o resultado:

Por que destaca 3 de fevereiro de 2014? Na minha cópia do AdventureWorks, não há pedidos de venda para essa data.

Agora, observe COUNT(soh.OrderDate) no código. Mais tarde, vamos esclarecer por que isso é tão importante.

Como obter nós sem filhos em relacionamentos pai-filho


Às vezes, precisamos conhecer os nós sem filho em um relacionamento pai-filho.

Vamos usar o banco de dados que usei em meu artigo sobre HierarchyID. Você precisa obter nós sem filhos em uma tabela de relacionamento pai-filho usando uma autojunção.
SELECT 
 r1.RankParentId
,r1.Rank AS RankParent
,r.RankId
FROM Ranks r
RIGHT JOIN Ranks r1 ON r.RankParentId = r1.RankId
WHERE r.RankId is NULL 

Avisos ao usar OUTER JOIN


Como um OUTER JOIN pode retornar linhas internas como um INNER JOIN, ele pode confundir. Problemas de desempenho também podem surgir. Então, observe os 3 pontos abaixo (eu volto a eles de vez em quando – não estou ficando mais jovem, então também esqueço).

Filtrando a tabela direita em um LEFT JOIN com um valor não nulo na cláusula WHERE


Pode ser um problema se você usou um LEFT OUTER JOIN, mas filtrou a tabela correta com um valor não nulo na cláusula WHERE. A razão é que ele se tornará funcionalmente equivalente a um INNER JOIN. Considere o exemplo abaixo:
USE AdventureWorks
GO

SELECT
 P.FirstName
,P.MiddleName
,P.LastName
,a.AddressLine1
,a.AddressLine2
,a.City
,adt.Name AS AddressType
FROM Person.Person p
LEFT JOIN Person.BusinessEntityAddress bea ON P.BusinessEntityID = bea.BusinessEntityID
LEFT JOIN Person.Address a ON bea.AddressID = a.AddressID
LEFT JOIN person.AddressType adt ON bea.AddressTypeID = adt.AddressTypeID
WHERE bea.AddressTypeID = 5 

A partir do código acima, vamos examinar as 2 tabelas:Person e BusinessEntityAddress . A Pessoa é a tabela à esquerda e BusinessEntityAddress é a mesa certa.

LEFT JOIN é usado, portanto, assume um BusinessEntityID nulo em algum lugar em BusinessEntityAddress . Aqui, observe a cláusula WHERE. Ele filtra a tabela correta com AddressTypeID =5. Ele descarta completamente todas as linhas externas em BusinessEntityAddress .

Isso pode ser qualquer um destes:
  • O desenvolvedor está testando algo no resultado, mas esqueceu de removê-lo.
  • INNER JOIN foi planejado, mas por algum motivo, LEFT JOIN foi usado.
  • O desenvolvedor não entende a diferença entre LEFT JOIN e INNER JOIN. Ele assume que qualquer um dos 2 funcionará, e isso não importa, porque os resultados são os mesmos neste caso.

Qualquer um dos 3 acima é ruim, mas a terceira entrada tem outra implicação. Vamos comparar o código acima com o equivalente INNER JOIN:
SELECT
 P.FirstName
,P.MiddleName
,P.LastName
,a.AddressLine1
,a.AddressLine2
,a.City
,adt.Name AS AddressType
FROM Person.Person p
INNER JOIN Person.BusinessEntityAddress bea ON P.BusinessEntityID = bea.BusinessEntityID
INNER JOIN Person.Address a ON bea.AddressID = a.AddressID
INNER JOIN person.AddressType adt ON bea.AddressTypeID = adt.AddressTypeID
WHERE bea.AddressTypeID = 5

Parece semelhante ao código anterior, exceto pelo tipo de junção. O resultado também é o mesmo, mas você deve observar as leituras lógicas em STATISTICS IO:

Na Figura 7, as primeiras estatísticas de E/S são do uso de INNER JOIN. Um total de leituras lógicas é 177. No entanto, as segundas estatísticas são para LEFT JOIN com um valor de leituras lógicas mais alto de 223. Assim, o uso incorreto de LEFT JOIN neste exemplo exigirá mais páginas ou recursos do SQL Server. Portanto, ele será executado mais lentamente.

Para levar


Se você pretende gerar linhas internas, use INNER JOIN. Caso contrário, não filtre a tabela direita em um LEFT JOIN com um valor não nulo. Se isso acontecer, você terá uma consulta mais lenta do que se usar INNER JOIN.

DICA DE BÔNUS :Esta situação também acontece em um RIGHT JOIN quando a tabela à esquerda é filtrada com um valor não nulo.

Uso incorreto de tipos de junção em uma junção múltipla


Suponha que queremos obter todos os fornecedores e o número de pedidos de compra de produtos para cada um. Aqui está o código:
USE AdventureWorks
GO

SELECT
 v.BusinessEntityID
,v.Name AS Vendor
,pod.ProductID
,pod.OrderQty
FROM Purchasing.Vendor v
LEFT JOIN Purchasing.PurchaseOrderHeader poh ON v.BusinessEntityID = poh.VendorID
LEFT JOIN Purchasing.PurchaseOrderDetail pod ON poh.PurchaseOrderID = pod.PurchaseOrderID 

O código acima retorna tanto os fornecedores com ordens de compra quanto aqueles sem. A Figura 8 mostra o Plano de Execução Real do código acima.

Pensando que todo pedido de compra tem um detalhe de pedido de compra garantido, um INNER JOIN seria melhor. No entanto, é realmente assim?

Primeiro, vamos ter o código modificado com o INNER JOIN.
USE AdventureWorks
GO

SELECT
 v.BusinessEntityID
,v.Name AS Vendor
,pod.ProductID
,pod.OrderQty
FROM Purchasing.Vendor v
LEFT JOIN Purchasing.PurchaseOrderHeader poh ON v.BusinessEntityID = poh.VendorID
INNER JOIN Purchasing.PurchaseOrderDetail pod ON poh.PurchaseOrderID = pod.PurchaseOrderID 

Lembre-se, o requisito acima diz “todos” os fornecedores. Como usamos o LEFT JOIN no código anterior, obteremos fornecedores sem pedidos de compra devolvidos. Isso se deve ao nulo PurchaseOrderID .

Alterar a junção para um INNER JOIN descartará todos os PurchaseOrderIDs nulos. Também cancelará todos os VendorIDs nulos do Fornecedor tabela. Com efeito, torna-se um INNER JOIN.

Isso é uma suposição correta? O Plano de Execução revelará a resposta:

Como você vê, todas as tabelas foram processadas usando INNER JOIN. Portanto, nossa suposição está correta. Mas, para piorar, o conjunto de resultados agora está incorreto porque os fornecedores sem pedidos não foram incluídos.

Para levar


Como no caso anterior, se você pretende um INNER JOIN, use-o. Mas você sabe o que fazer se encontrar uma situação como esta aqui.

Nesse caso, um INNER JOIN descartará todas as linhas externas até a tabela superior no relacionamento. Mesmo que sua outra junção seja uma LEFT JOIN, isso não importa. Provamos isso nos Planos de Execução.

Uso incorreto de COUNT() em associações externas


Lembra do nosso código de exemplo que conta o número de pedidos por data e o resultado na Figura 6?

Aqui, vamos esclarecer por que 02/03/2014 está destacado e sua relação com COUNT(soh.OrderDate) .

Se você tentar usar COUNT(*), o número de pedidos para essa data se tornará 1, o que está errado. Não há pedidos nessa data. Portanto, ao usar COUNT() com um OUTER JOIN, use a coluna correta para contar.

No nosso caso, soh.OrderDate pode ser nulo ou não. Quando não for nulo, COUNT() incluirá a linha na contagem. COUNT(*) fará com que conte tudo, incluindo os nulos. E no final, resultados errados.

As conclusões do OUTER JOIN


Vamos resumir os pontos:
  • OUTER JOIN pode retornar linhas internas e externas. As linhas internas são o resultado semelhante ao resultado do INNER JOIN. As linhas externas são os valores não nulos com suas contrapartes nulas com base na condição de junção.
  • OUTER JOIN pode ser LEFT, RIGHT ou FULL. Tínhamos exemplos para cada um.
  • As linhas externas retornadas por OUTER JOIN podem ser usadas de várias maneiras práticas. Tivemos ideias sobre quando você pode usar essas coisas.
  • Também tivemos ressalvas ao usar OUTER JOIN. Esteja atento aos 3 pontos acima para evitar bugs e problemas de desempenho.

A parte final desta série discutirá o CROSS JOIN. Então, até lá. E se você gostou deste post, compartilhe um pouco de amor clicando nos botões de mídia social. Boa codificação!