sql >> Base de Datos >  >> RDS >> Database

Su guía definitiva para uniones SQL:UNIÓN EXTERNA - Parte 2

La unión externa está en el centro del escenario hoy. Y esta es la parte 2 de su guía definitiva para uniones SQL. Si te perdiste la parte 1, aquí está el enlace.

Por lo que parece, exterior es lo opuesto a interior. Sin embargo, si considera la combinación externa de esta manera, se confundirá. Para colmo, no tienes que incluir la palabra exterior en su sintaxis explícitamente. ¡Es opcional!

Pero antes de sumergirnos, analicemos los valores nulos relacionados con las uniones externas.

Nulos y UNIÓN EXTERNA

Cuando une 2 tablas, uno de los valores de cualquiera de las tablas puede ser nulo. Para INNER JOIN, los registros con valores nulos no coincidirán y se descartarán y no aparecerán en el conjunto de resultados. Si desea obtener los registros que no coinciden, su única opción es OUTER JOIN.

Volviendo a los antónimos, ¿no es lo contrario de INNER JOIN? No del todo, como verás en la siguiente sección.

Todo sobre SQL Server OUTER JOIN

Comprender las uniones externas comienza con la salida. Aquí hay una lista completa de lo que puede esperar:

  • Todos los registros que coincidan con la condición de combinación o el predicado. Esa es la expresión justo después de la palabra clave ON, muy parecida a la salida INNER JOIN. Nos referimos a este problema como la fila interior .
  • Valores no NULL de la izquierda tabla con las contrapartes nulas de la derecha mesa. Nos referimos a este problema como filas externas .
  • Valores no NULL de la derecha tabla con las contrapartes nulas de la izquierda mesa. Esta es otra forma de filas exteriores.
  • Finalmente, podría ser una combinación de todas las cosas descritas anteriormente.

Con esa lista, podemos decir que OUTER JOIN devuelve filas internas y externas .

  • Interna:porque los resultados exactos de INNER JOIN pueden ser devuelto.
  • Exterior:porque las filas exteriores también pueden ser devuelto.

Es la diferencia con INNER JOIN.

LAS UNIONES INTERNAS DEVUELVEN SOLAMENTE LAS FILAS INTERIORES. LAS UNIONES EXTERNAS PUEDEN DEVOLVER FILAS INTERNAS Y EXTERNAS

Tenga en cuenta que usé "puede ser" y "también puede ser". Depende de su cláusula WHERE (o si alguna vez incluye una cláusula WHERE) si devuelve filas tanto internas como externas.

Pero a partir de una declaración SELECT, ¿cómo puede determinar cuál es la tabla izquierda o derecha? ? ¡Buena pregunta!

¿Cómo saber cuál es la tabla izquierda o derecha en una combinación?

Podemos responder a esta pregunta con ejemplos:

SELECT *
FROM Table1 a
LEFT OUTER JOIN Table2 b on a.column1 = b.column1

Del ejemplo anterior, Table1 es la tabla de la izquierda, y Table2 es la mesa de la derecha. Ahora, veamos otro ejemplo. Esta vez, se trata de una unión múltiple simple.

SELECT *
FROM Table1 a
LEFT OUTER JOIN Table2 b on a.column1 = b.column1
LEFT OUTER JOIN Table3 c on b.column2 = c.column1

En este caso, para saber qué queda o qué queda a la derecha, recuerda que un join funciona en 2 tablas.

Tabla 1 sigue siendo la tabla de la izquierda, y Table2 es la mesa de la derecha. Esto se refiere a unir 2 tablas:Table1 y Tabla2 . ¿Qué hay de unirse a Table2? y Tabla3 ? Tabla 2 se convierte en la tabla de la izquierda y Table3 es la tabla correcta.

Si añadimos una cuarta tabla, Table3 se convierte en la tabla de la izquierda y Table4 es la mesa de la derecha. Pero no termina ahí. Podemos unir otra tabla a la Table1 . He aquí un ejemplo:

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

Tabla 1 es la tabla de la izquierda, y Table5 es la mesa de la derecha. También puedes hacer lo mismo con las otras tablas.

Bien, volvamos a la lista de resultados esperados anterior. También podemos derivar los tipos de uniones externas a partir de estos.

Tipos de uniones externas

Hay 3 tipos basados ​​en las salidas OUTER JOIN.

UNIÓN EXTERNA IZQUIERDA (UNIÓN IZQUIERDA)

LEFT JOIN devuelve filas internas + valores no NULL desde la izquierda tabla con las contrapartes nulas de la tabla derecha. Por lo tanto, es LEFT JOIN porque la tabla de la izquierda es la dominante de las dos tablas dentro de la unión que tienen valores no nulos.

EJEMPLO 1 DE UNIÓN EXTERNA IZQUIERDA
-- 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 

En el ejemplo anterior, el Cliente es la tabla de la izquierda y SalesOrderHeader es la mesa de la derecha. El resultado de la consulta es 32.166 registros – incluye filas internas y externas. Puede ver una parte en la Figura 1:

Supongamos que queremos devolver solo las filas exteriores o los clientes sin pedidos. Para hacer eso, agregue una cláusula WHERE para incluir solo filas con valores 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

El conjunto de resultados que obtuve es 701 registros . A todos les gusta el OrderDate nulo de la figura 1.

Si solo obtengo las filas internas, el resultado será 31 465 registros . Puedo hacerlo cambiando la cláusula WHERE para incluir esos SalesOrderIDs que no son nulos. O puedo cambiar la combinación a INNER JOIN y eliminar la cláusula WHERE.

Para ver si sale del resultado del primer ejemplo sin la cláusula WHERE, resumamos los registros.

Filas internas Filas exteriores Filas totales
31 465 registros 701 registros 32.166 registros

Del total de filas anterior con 32 166 registros, puede ver que se verifica con los resultados del primer ejemplo. Esto también muestra cómo funciona LEFT OUTER JOIN.

EJEMPLO 2 DE UNIÓN EXTERNA IZQUIERDA

Esta vez, el ejemplo es una unión múltiple. Tenga en cuenta también que eliminamos la palabra clave 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 

Generó 19.996 registros. Puede consultar la parte de la salida en la Figura 2 a continuación. Los registros con nulo AddressLine1 son filas exteriores. Arriba hay filas internas.

UNIÓN EXTERNA DERECHA (UNIÓN DERECHA)

RIGHT JOIN devuelve filas internas + valores no NULL de la derecha tabla con las contrapartes nulas de la tabla de la izquierda.

EJEMPLO 1 DE UNIÓN EXTERNA DERECHA
-- 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 

La Figura 3 muestra 10 de 501 registros en el conjunto de resultados.

En el ejemplo anterior, ProductReview es la tabla de la izquierda, y el Producto es la mesa de la derecha. Dado que se trata de una UNIÓN EXTERNA DERECHA, tenemos la intención de incluir los valores no NULL de la tabla de la derecha.

Sin embargo, elegir entre LEFT JOIN o RIGHT JOIN depende de usted. ¿Por qué? Porque puede expresar la consulta, ya sea LEFT o RIGHT JOIN, y obtener los mismos resultados. Intentémoslo con 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

Intente ejecutar lo anterior y obtendrá el mismo resultado que en la Figura 3. Pero, ¿cree que el Optimizador de consultas los tratará de manera diferente? Descubrámoslo en el Plan de Ejecución de ambos en la Figura 4.

Si es nuevo en esto, hay algunas sorpresas en el Plan de Ejecución.

  1. Los diagramas tienen el mismo aspecto y lo son:prueba un Comparar plan de presentación , y verá el mismo QueryPlanHash .
  2. Observe el diagrama superior con una combinación Merge. Usamos una UNIÓN EXTERNA DERECHA, pero SQL Server la cambió a UNIÓN EXTERNA IZQUIERDA. También cambió las tablas izquierda y derecha. Hace que sea igual a la segunda consulta con LEFT JOIN.

Como ves ahora, los resultados son los mismos. Por lo tanto, elija cuál de los OUTER JOIN será más conveniente.

¿Por qué SQL Server cambió RIGHT JOIN a LEFT JOIN?

El motor de la base de datos no tiene que seguir la forma en que expresa las uniones lógicas. Siempre que pueda producir resultados correctos de la manera más rápida que cree posible, hará cambios. Incluso atajos.

No concluya que RGHT JOIN es malo y LEFT JOIN es bueno.

EJEMPLO 2 DE UNIÓN EXTERNA DERECHA

Eche un vistazo al siguiente ejemplo:

-- 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 

Hay 2 cosas que puede obtener de esta consulta, como puede ver en la Figura 5 a continuación:

Los resultados de la consulta muestran lo siguiente:

  1. Las direcciones no asignadas:estos registros son aquellos con nombres nulos.
  2. Tipos de direcciones sin direcciones. Los tipos de dirección de archivo, facturación y principal no tienen direcciones correspondientes. Esos son de los registros 817 a 819.

UNIÓN EXTERNA COMPLETA (UNIÓN COMPLETA)

FULL JOIN devuelve una combinación de filas internas y filas externas, izquierda y derecha.

-- 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

El conjunto de resultados incluye 20.815 registros. Como cabría esperar, es una cantidad total de registros del conjunto de resultados de INNER JOIN, LEFT JOIN y RIGHT JOIN.

LEFT y RIGHT JOIN incluyen una cláusula WHERE para mostrar solo los resultados con valores nulos en las tablas izquierda o derecha.

UNIÓN INTERNA UNIR A LA IZQUIERDA
(DONDE a.AddressID ES NULO)
UNIÓN DERECHA
(DONDE P. BusinessEntityID ES NULO)
TOTAL (Igual que UNIÓN COMPLETA)
18 798 registros 1198 registros 819 registros 20.815 registros

Tenga en cuenta que FULL JOIN puede producir un gran conjunto de resultados a partir de tablas grandes. Por lo tanto, utilícelo solo cuando lo necesite.

Usos prácticos de OUTER JOIN

Si aún dudas sobre cuándo puedes y debes usar OUTER JOIN, aquí tienes algunas ideas.

Uniones externas que generan tanto filas internas como externas

Ejemplos pueden ser:

  • Lista alfabética de pedidos de clientes pagados y no pagados.
  • Lista alfabética de empleados con tardanzas o sin registro de tardanzas.
  • Una lista de asegurados que renovaron y no renovaron sus pólizas de seguro más recientes.

Uniones externas que generan solo filas externas

Los ejemplos incluyen:

  • lista alfabética de empleados sin registro de tardanzas para el premio de cero tardanzas
  • lista de territorios sin clientes
  • lista de agentes de ventas sin ventas de un producto en particular
  • obtener resultados de valores faltantes, como fechas sin pedidos de venta en un período determinado (ejemplo a continuación)
  • nodos sin hijos en una relación padre-hijo (ejemplo a continuación)

Obtención de resultados de valores perdidos

Supongamos que necesita producir un informe. Ese informe debe mostrar la cantidad de días de cada mes en un período determinado en el que no hubo pedidos. El Encabezado de pedido de ventas en AdventureWorks contiene las Fechas de pedido , pero no tienen fechas sin pedidos. ¿Qué puedes hacer?

1. Crear una tabla de todas las fechas en un período

Una secuencia de comandos de muestra a continuación creará una tabla de fechas para todo 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 mostrar los días sin 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]

El código anterior cuenta el número de días en que no se han realizado pedidos. Cabecera de pedido de venta contiene las fechas con los pedidos. Por lo tanto, los valores nulos devueltos en la combinación contarán como días sin pedidos.

Mientras tanto, si quieres saber las fechas exactas, puedes eliminar el conteo y la agrupación.

SELECT
 d.d
,soh.OrderDate
FROM Dates d
LEFT JOIN Sales.SalesOrderHeader soh ON d.d = soh.OrderDate
WHERE soh.OrderDate IS NULL

O, si desea contar los pedidos en un período determinado y ver qué fecha tiene cero pedidos, así es 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

El código anterior cuenta los pedidos de febrero de 2014. Vea el resultado:

¿Por qué destaca el 3 de febrero de 2014? En mi copia de AdventureWorks, no hay órdenes de venta para esa fecha.

Ahora, observe COUNT(soh.OrderDate) en el código. Más adelante, aclararemos por qué esto es tan importante.

Obtención de nodos sin hijos en relaciones padre-hijo

A veces necesitamos conocer los nodos sin hijos en una relación padre-hijo.

Usemos la base de datos que he usado en mi artículo sobre HierarchyID. Necesita obtener nodos sin hijos en una tabla de relaciones padre-hijo mediante una autounión.

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 

Advertencias al usar OUTER JOIN

Dado que un OUTER JOIN puede devolver filas internas como un INNER JOIN, puede confundir. Los problemas de rendimiento también pueden surgir. Por lo tanto, tenga en cuenta los 3 puntos a continuación (recurro a ellos de vez en cuando; no me estoy volviendo más joven, así que también lo olvido).

Filtrar la tabla derecha en LEFT JOIN con un valor no nulo en la cláusula WHERE

Puede ser un problema si usó LEFT OUTER JOIN pero filtró la tabla derecha con un valor no nulo en la cláusula WHERE. La razón es que será funcionalmente equivalente a un INNER JOIN. Considere el siguiente ejemplo:

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 

Del código anterior, examinemos las 2 tablas:Persona y dirección de entidad comercial . La Persona es la tabla de la izquierda y BusinessEntityAddress es la tabla correcta.

Se usa LEFT JOIN, por lo que asume un BusinessEntityID nulo en algún lugar de BusinessEntityAddress . Aquí, observe la cláusula WHERE. Filtra la tabla correcta con AddressTypeID =5. Descarta por completo todas las filas exteriores en BusinessEntityAddress .

Esto puede ser cualquiera de estos:

  • El desarrollador está probando algo en el resultado pero olvidó eliminarlo.
  • Se pretendía INNER JOIN, pero por alguna razón, se utilizó LEFT JOIN.
  • El desarrollador no comprende la diferencia entre LEFT JOIN e INNER JOIN. Él asume que cualquiera de los 2 funcionará, y no importa porque los resultados son los mismos en este caso.

Cualquiera de los 3 anteriores es malo, pero la tercera entrada tiene otra implicación. Comparemos el código anterior con el equivalente de 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

Se parece al código anterior excepto por el tipo de combinación. El resultado también es el mismo, pero debe tener en cuenta las lecturas lógicas en ESTADÍSTICAS IO:

En la Figura 7, las primeras estadísticas de E/S provienen del uso de INNER JOIN. Un total de lecturas lógicas es 177. Sin embargo, las segundas estadísticas son para LEFT JOIN con un valor de lecturas lógicas más alto de 223. Por lo tanto, el uso incorrecto de LEFT JOIN en este ejemplo requerirá más páginas o recursos de SQL Server. Por lo tanto, funcionará más lento.

Para llevar

Si tiene la intención de generar filas internas, use INNER JOIN. De lo contrario, no filtre la tabla derecha en LEFT JOIN con un valor no nulo. Si esto sucede, terminará con una consulta más lenta que si usa INNER JOIN.

CONSEJO ADICIONAL :Esta situación también ocurre en un RIGHT JOIN cuando la tabla de la izquierda se filtra con un valor no nulo.

Uso incorrecto de tipos de combinación en una combinación múltiple

Supongamos que queremos obtener todos los proveedores y el número de órdenes de compra de productos para cada uno. Aquí está el 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 

El código anterior devuelve tanto a los proveedores con órdenes de compra como a los que no. La figura 8 muestra el plan de ejecución real del código anterior.

Pensando que cada orden de compra tiene un detalle de orden de compra garantizado, sería mejor un INNER JOIN. Sin embargo, ¿es realmente así?

Primero, tengamos el código modificado con 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 

Recuerde, el requisito anterior dice "todos" los proveedores. Dado que usamos LEFT JOIN en el código anterior, obtendremos proveedores sin órdenes de compra devueltas. Eso se debe al nulo PurchaseOrderID .

Cambiar la unión a INNER JOIN descartará todos los PurchaseOrderIDs. nulos. También cancelará todos los VendorIDs nulos. del proveedor mesa. En efecto, se convierte en INNER JOIN.

¿Es esa una suposición correcta? El Plan de Ejecución revelará la respuesta:

Como puede ver, todas las tablas se procesaron utilizando INNER JOIN. Por lo tanto, nuestra suposición es correcta. Pero lo peor de todo es que el conjunto de resultados ahora es incorrecto porque no se incluyeron los proveedores sin pedidos.

Para llevar

Como en el caso anterior, si pretende un INNER JOIN, utilícelo. Pero ya sabes qué hacer si te encuentras con una situación como la de aquí.

En este caso, INNER JOIN descartará todas las filas externas hasta la tabla superior de la relación. Incluso si su otra unión es LEFT JOIN, no importará. Lo hemos demostrado en los Planes de Ejecución.

Uso incorrecto de COUNT() en combinaciones externas

¿Recuerda nuestro código de muestra que cuenta el número de pedidos por fecha y el resultado en la Figura 6?

Aquí, aclararemos por qué se resalta el 03/02/2014 y su relación con COUNT(soh.OrderDate) .

Si intenta usar COUNT (*), la cantidad de pedidos para esa fecha se convierte en 1, lo cual es incorrecto. No hay pedidos en esa fecha. Entonces, cuando use COUNT() con OUTER JOIN, use la columna correcta para contar.

En nuestro caso, soh.OrderDate puede ser nulo o no. Cuando no es nulo, COUNT() incluirá la fila en el conteo. COUNT (*) hará que cuente todo, incluidos los valores nulos. Y al final, resultados erróneos.

Las conclusiones de OUTER JOIN

Resumamos los puntos:

  • OUTER JOIN puede devolver filas internas y filas externas. Las filas internas son el resultado similar al resultado de INNER JOIN. Las filas externas son los valores no nulos con sus contrapartes nulas según la condición de unión.
  • UNION EXTERNA puede ser IZQUIERDA, DERECHA o COMPLETA. Teníamos ejemplos para cada uno.
  • Las filas externas devueltas por OUTER JOIN se pueden usar de varias maneras prácticas. Teníamos ideas sobre cuándo puedes usar estas cosas.
  • También teníamos advertencias al usar OUTER JOIN. Tenga en cuenta los 3 puntos anteriores para evitar errores y problemas de rendimiento.

La parte final de esta serie discutirá CROSS JOIN. Entonces, hasta entonces. Y si te gusta esta publicación, comparte un poco de amor haciendo clic en los botones de las redes sociales. ¡Feliz codificación!