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

Errores, trampas y mejores prácticas de T-SQL:uniones

Este artículo es la tercera entrega de una serie sobre errores, trampas y mejores prácticas de T-SQL. Anteriormente cubrí el determinismo y las subconsultas. Esta vez me concentro en las uniones. Algunos de los errores y mejores prácticas que cubro aquí son el resultado de una encuesta que hice entre otros MVP. ¡Gracias Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man y Paul White por ofrecer sus ideas!

En mis ejemplos, usaré una base de datos de muestra llamada TSQLV5. Puede encontrar el script que crea y completa esta base de datos aquí, y su diagrama ER aquí.

En este artículo me enfoco en cuatro errores comunes clásicos:COUNT (*) en uniones externas, agregados de doble inmersión, contradicción EN DONDE y contradicción de unión EXTERIOR-INTERIOR. Todos estos errores están relacionados con los fundamentos de consulta de T-SQL y son fáciles de evitar si sigue las prácticas recomendadas simples.

COUNT(*) en combinaciones externas

Nuestro primer error tiene que ver con recuentos incorrectos informados para grupos vacíos como resultado del uso de una combinación externa y el agregado COUNT(*). Considere la siguiente consulta calculando el número de pedidos y el flete total por cliente:

 USAR TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SELECT custid, COUNT(*) AS numorders, SUM(flete) AS totalfreight FROM Sales.Orders GROUP BY custid ORDER BY custid;

Esta consulta genera el siguiente resultado (abreviado):

 custid numorders totalfreight ------- ---------- ------------- 1 6 225.58 2 4 97.42 3 7 268.52 4 13 471.95 5 18 1559,52 ... 21 7 232,75 23 5 637,94 ... 56 10 862,74 58 6 277,96 ... 87 15 822,48 88 9 194,71 89 14 1353,06 90 7 88,41 91 7 175,74 (filas afectadas)

Actualmente hay 91 clientes presentes en la tabla Clientes, de los cuales 89 realizaron pedidos; por lo tanto, el resultado de esta consulta muestra 89 grupos de clientes y su recuento de pedidos correcto y los agregados totales de flete. Los clientes con ID 22 y 57 están presentes en la tabla Clientes pero no han realizado ningún pedido y, por lo tanto, no aparecen en el resultado.

Suponga que se le solicita que incluya clientes que no tienen ningún pedido relacionado en el resultado de la consulta. Lo natural que se debe hacer en tal caso es realizar una unión externa izquierda entre Clientes y Pedidos para conservar a los clientes sin pedidos. Sin embargo, un error típico al convertir la solución existente a una que aplica la combinación es dejar el cálculo del recuento de pedidos como COUNT(*), como se muestra en la siguiente consulta (llámela Consulta 1):

 SELECT C.custid, COUNT(*) AS numorders, SUM(O.fleight) AS totalfreight FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C.custid PEDIDO POR C.custid;

Esta consulta genera el siguiente resultado:

 custid numorders totalfreight ------- ---------- ------------- 1 6 225.58 2 4 97.42 3 7 268.52 4 13 471.95 5 18 1559.52 ... 21 7 232.75 22 1 NULL 23 5 637.94 ... 56 10 862.74 57 1 NULL 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 81 74 1 9 hileras afectadas pre> 

Observe que los clientes 22 y 57 aparecen esta vez en el resultado, pero su recuento de pedidos muestra 1 en lugar de 0 porque COUNT(*) cuenta filas y no pedidos. El flete total se informa correctamente porque SUM(flete) ignora las entradas NULL.

El plan para esta consulta se muestra en la Figura 1.

Figura 1:Plan para la consulta 1

En este plan, Expr1002 representa el recuento de filas por grupo que, como resultado de la unión externa, se establece inicialmente en NULL para los clientes sin pedidos coincidentes. El operador Compute Scalar justo debajo del nodo raíz SELECT luego convierte NULL en 1. Ese es el resultado de contar filas en lugar de contar órdenes.

Para corregir este error, desea aplicar el agregado COUNT a un elemento del lado no conservado de la unión externa y desea asegurarse de que use una columna que no admita valores NULL como entrada. La columna de clave principal sería una buena opción. Aquí está la consulta de solución (llámela Consulta 2) con el error corregido:

 SELECT C.custid, COUNT(O.orderid) AS numorders, SUM(O.careight) AS totalfreight FROM Ventas.Clientes AS C LEFT OUTER JOIN Ventas.Pedidos AS O ON C.custid =O.custid GROUP BY C .custid ORDEN POR C.custid;

Aquí está el resultado de esta consulta:

 custid numorders totalfreight ------- ---------- ------------- 1 6 225.58 2 4 97.42 3 7 268.52 4 13 471.95 5 18 1559,52 ... 21 7 232,75 22 0 NULL 23 5 637,94 ... 56 10 862,74 57 0 NULL 58 6 277,96 ... 87 15 822,48 88 9 194,71 89 14 1353,06 90 7 81 74 1 9 filas afectadas pre> 

Observe que esta vez los clientes 22 y 57 muestran la cuenta correcta de cero.

El plan para esta consulta se muestra en la Figura 2.

Figura 2:Plan para la Consulta 2

También puede ver el cambio en el plan, donde un NULL que representa el recuento de un cliente sin pedidos coincidentes se convierte en 0 y no en 1 esta vez.

Cuando utilice uniones, tenga cuidado al aplicar el agregado COUNT(*). Cuando se usan uniones externas, generalmente es un error. La mejor práctica es aplicar el agregado COUNT a una columna que no acepta valores NULL desde el lado de muchos de la combinación de uno a muchos. La columna de clave principal es una buena opción para este propósito, ya que no permite valores NULL. Esta podría ser una buena práctica incluso cuando se usan uniones internas, ya que nunca se sabe si en un momento posterior necesitará cambiar una unión interna por una externa debido a un cambio en los requisitos.

Áridos de doble inmersión

Nuestro segundo error también implica mezclar uniones y agregados, esta vez teniendo en cuenta los valores de origen varias veces. Considere la siguiente consulta como ejemplo:

 SELECT C.custid, COUNT(O.orderid) AS numorders, SUM(O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC(12 , 2)) AS totalval FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C.custid ORDER POR C.custid;

Esta consulta une Customers, Orders y OrderDetails, agrupa las filas por custid y se supone que calcula agregados como el recuento de pedidos, el flete total y el valor total por cliente. Esta consulta genera el siguiente resultado:

 custid numorders totalfreight totalval ------- ---------- ------------- --------- 1 12 419.60 4273.00 2 10 306.59 1402.95 3 17 667.29 7023.98 4 30 1447.14 13390.65 5 52 4835.18 24927. 

¿Puedes encontrar el error aquí?

Los encabezados de pedido se almacenan en la tabla Pedidos y sus respectivas líneas de pedido se almacenan en la tabla Detalles del pedido. Cuando une encabezados de orden con sus respectivas líneas de orden, el encabezado se repite en el resultado de la combinación por línea. Como resultado, el agregado COUNT(O.orderid) refleja incorrectamente el recuento de líneas de pedido y no el recuento de pedidos. De manera similar, SUM(O.freight) tiene en cuenta incorrectamente el flete varias veces por pedido, tantas como la cantidad de líneas de pedido dentro del pedido. El único cálculo agregado correcto en esta consulta es el que se utiliza para calcular el valor total, ya que se aplica a los atributos de las líneas de pedido:SUM(OD.qty * OD.unitprice * (1 – OD.discount).

Para obtener el recuento de pedidos correcto, basta con utilizar un agregado de recuento distinto:COUNT(DISTINCT O.orderid). Podría pensar que se puede aplicar la misma solución al cálculo del flete total, pero esto solo introduciría un nuevo error. Aquí está nuestra consulta con distintos agregados aplicados a las medidas del encabezado del pedido:

 SELECT C.custid, COUNT(DISTINCT O.orderid) AS numorders, SUM(DISTINCT O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC (12, 2)) AS totalval FROM Ventas.Clientes AS C LEFT OUTER JOIN Ventas.Pedidos AS O ON C.custid =O.custid LEFT OUTER JOIN Ventas.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C. custid ORDEN POR C.custid;

Esta consulta genera el siguiente resultado:

 custid numorders totalfreight totalval ------- ---------- ------------- --------- 1 6 225.58 4273.00 2 4 97.42 1402.95 3 7 268.52 7023.98 4 13 448.23 13390.65 ***** 5 18 1559.52 24927.58 ... 87 15 822.48 15648.70 88 9 194.71 6068.20 89 14 1353.06 27363.6 90 7 87.66. /pre> 

Los recuentos de pedidos ahora son correctos, pero los valores totales de flete no lo son. ¿Puedes detectar el nuevo error?

El nuevo error es más elusivo porque se manifiesta solo cuando el mismo cliente tiene al menos un caso en el que varios pedidos tienen exactamente los mismos valores de flete. En tal caso, ahora está teniendo en cuenta el flete solo una vez por cliente, y no una vez por pedido como debería.

Utilice la siguiente consulta (requiere SQL Server 2017 o superior) para identificar valores de flete no distintos para el mismo cliente:

 CON C AS ( SELECCIONE id. de pedido, flete, STRING_AGG(CAST(id. de pedido AS VARCHAR(MAX)), ', ') DENTRO DEL GRUPO(ORDEN POR id.de pedido) AS pedidos DESDE Ventas.GRUPO DE ÓRDENES POR id. de cliente, flete CONTANDO(* )> 1 ) SELECT custid, STRING_AGG(CONCAT('(flete:', flete, ', pedidos:', pedidos, ')'), ', ') as duplicates FROM C GROUP BY custid;

Esta consulta genera el siguiente resultado:

 custid duplicados ------- -------------------------------------- - 4 (flete:23,72, pedidos:10743, 10953) 90 (flete:0,75, pedidos:10615, 11005)

Con estos resultados, se da cuenta de que la consulta con el error informó valores totales de flete incorrectos para los clientes 4 y 90. La consulta informó valores totales de flete correctos para el resto de los clientes, ya que sus valores de flete resultaron ser únicos.

Para corregir el error, debe separar el cálculo de los agregados de pedidos y de las líneas de pedido en diferentes pasos utilizando expresiones de tabla, así:

 CON O AS ( SELECT custid, COUNT(orderid) AS numorders, SUM(flete) AS totalfreight FROM Sales.Orders GROUP BY custid ), OD AS ( SELECT O.custid, CAST(SUM(OD.qty * OD. precio unitario * (1 - OD.discount)) AS NUMERIC(12, 2)) AS totalval FROM Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY O.custid ) SELECT C. custid, O.numorders, O.totalfreight, OD.totalval FROM Ventas.Clientes COMO C IZQUIERDA EXTERNA JOIN O ON C.custid =O.custid IZQUIERDA OUTER JOIN OD ON C.custid =OD.custid ORDEN POR C.custid;

Esta consulta genera el siguiente resultado:

 custid numorders totalfreight totalval ------- ---------- ------------- --------- 1 6 225.58 4273.00 2 4 97.42 1402.95 3 7 268.52 7023.98 4 13 471.95 13390.65 ***** 5 18 1559.52 24927.58 ... 87 15 822.48 15648.70 88 9 194.71 6068.20 89 14 1353.06 27363.6 90 7 88.4111. /pre> 

Observe que los valores totales de flete para los clientes 4 y 90 ahora son más altos. Estos son los números correctos.

La mejor práctica aquí es tener cuidado al unir y agregar datos. Desea estar alerta a tales casos cuando se unen varias tablas y se aplican agregados a medidas de una tabla que no es una tabla de borde o de hoja en las uniones. En tal caso, generalmente necesita aplicar los cálculos agregados dentro de las expresiones de la tabla y luego unir las expresiones de la tabla.

Entonces, el error de los agregados de doble inmersión está solucionado. Sin embargo, existe potencialmente otro error en esta consulta. ¿Puedes distinguirlo? Proporcionaré los detalles sobre un error potencial como el cuarto caso que cubriré más adelante en "Contradicción de unión EXTERIOR-INTERIOR".

Contradicción EN-DÓNDE

Nuestro tercer error es el resultado de confundir los roles que se supone que deben desempeñar las cláusulas ON y WHERE. Como ejemplo, suponga que se le asignó una tarea para hacer coincidir los clientes y los pedidos que realizaron desde el 12 de febrero de 2019, pero también incluir en la salida a los clientes que no realizaron pedidos desde entonces. Intenta resolver la tarea usando la siguiente consulta (llámela Consulta 3):

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212';

Cuando se usa una combinación interna, tanto ON como WHERE desempeñan los mismos roles de filtrado y, por lo tanto, no importa cómo organice los predicados entre estas cláusulas. Sin embargo, cuando se usa una combinación externa como en nuestro caso, estas cláusulas tienen significados diferentes.

La cláusula ON juega un papel coincidente, lo que significa que se devolverán todas las filas del lado preservado de la combinación (Clientes en nuestro caso). Los que tienen coincidencias basadas en el predicado ON se conectan con sus coincidencias y, como resultado, se repiten por coincidencia. Los que no tienen coincidencias se devuelven con NULL como marcadores de posición en los atributos del lado no preservado.

Por el contrario, la cláusula WHERE juega un papel de filtrado más simple, siempre. Esto significa que se devuelven las filas para las que el predicado de filtrado se evalúa como verdadero y el resto se descarta. Como resultado, algunas de las filas del lado conservado de la combinación se pueden eliminar por completo.

Recuerde que los atributos del lado no conservado de la unión externa (Órdenes en nuestro caso) se marcan como NULL para las filas externas (no coincidencias). Cada vez que aplica un filtro que involucra un elemento del lado no conservado de la combinación, el predicado del filtro se evalúa como desconocido para todas las filas externas, lo que resulta en su eliminación. Esto está de acuerdo con la lógica de predicados de tres valores que sigue SQL. Efectivamente, la unión se convierte en una unión interna como resultado. La única excepción a esta regla es cuando busca específicamente un NULL en un elemento del lado no conservado para identificar las no coincidencias (el elemento IS NULL).

Nuestra consulta con errores genera el siguiente resultado:

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Cliente NRZBB 11011 2019-04-09 1 Cliente NRZBB 10952 2019-03-16 2 Cliente MLTDN 10926 2019-03-04 4 Cliente HFBZG 11016 2019-04-10 4 Cliente HFBZG 10953 2019-03-16 4 Cliente HFBZG 10920 3-01920 03 5 Cliente HGVLZ 10924 2019-03-04 6 Cliente XHXJV 11058 2019-04-29 6 Cliente XHXJV 10956 2019-03-17 8 Cliente QUHWH 10970 2019-03-24 ... 20 Cliente THHDP 10979 2019-03-26 Cliente THHDP 10968 2019-03-23 ​​20 Cliente THHDP 10895 2019-02-18 24 Cliente CYZTN 11050 2019-04-27 24 Cliente CYZTN 11001 2019-04-06 24 Cliente CYZTN 10993 2019-04-01 ... (195 filas afectados)

Se supone que el resultado deseado tiene 213 filas, incluidas 195 filas que representan los pedidos que se realizaron desde el 12 de febrero de 2019 y 18 filas adicionales que representan a los clientes que no han realizado pedidos desde entonces. Como puede ver, el resultado real no incluye a los clientes que no han realizado pedidos desde la fecha especificada.

El plan para esta consulta se muestra en la Figura 3.

Figura 3:Plan para la consulta 3

Observe que el optimizador detectó la contradicción e internamente convirtió la combinación externa en una combinación interna. Es bueno verlo, pero al mismo tiempo es una clara indicación de que hay un error en la consulta.

He visto casos en los que las personas intentaron corregir el error agregando el predicado OR O.orderid IS NULL a la cláusula WHERE, así:

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212' O O.orderid ES NULO;

El único predicado coincidente es el que compara los ID de cliente de los dos lados. Por lo tanto, la combinación en sí misma devuelve los clientes que realizaron pedidos en general, junto con sus pedidos coincidentes, así como los clientes que no realizaron ningún pedido, con valores NULL en sus atributos de pedido. Luego, los predicados de filtrado filtran a los clientes que realizaron pedidos desde la fecha especificada, así como a los clientes que no han realizado ningún pedido (clientes 22 y 57). En la consulta faltan clientes que realizaron algunos pedidos, ¡pero no desde la fecha especificada!

Esta consulta genera el siguiente resultado:

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Cliente NRZBB 11011 2019-04-09 1 Cliente NRZBB 10952 2019-03-16 2 Cliente MLTDN 10926 2019-03-04 4 Cliente HFBZG 11016 2019-04-10 4 Cliente HFBZG 10953 2019-03-16 4 Cliente HFBZG 10920 3-01920 03 5 Cliente HGVLZ 10924 2019-03-04 6 Cliente XHXJV 11058 2019-04-29 6 Cliente XHXJV 10956 2019-03-17 8 Cliente QUHWH 10970 2019-03-24 ... 20 Cliente THHDP 10979 2019-03-26 Cliente THHDP 10968 2019-03-23 ​​20 Cliente THHDP 10895 2019-02-18 22 Cliente DTDMN NULL NULL 24 Cliente CYZTN 11050 2019-04-27 24 Cliente CYZTN 11001 2019-04-06 24 Cliente CYZTN 10993 4-2019-0. .. (197 filas afectadas)

Para solucionar correctamente el error, necesita que tanto el predicado que compara los ID de cliente de los dos lados como el predicado con la fecha del pedido se consideren predicados coincidentes. Para lograr esto, ambos deben especificarse en la cláusula ON, así (llame a esta Consulta 4):

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid AND O.orderdate>='20190212';

Esta consulta genera el siguiente resultado:

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Cliente NRZBB 11011 2019-04-09 1 Cliente NRZBB 10952 2019-03-16 2 Cliente MLTDN 10926 2019-03-04 3 Cliente KBUDE NULL NULL 4 Cliente HFBZG 11016 2019-04-10 4 Cliente HFBZG 10953 2019-03-16 4 Cliente HFBZG 10920 2019-03-03 5 Cliente HGVLZ 10924 2019-03-04 6 Cliente XHXJV 11058 2019-04-29 6 Cliente XHXJV 10956 2019-03-17 7 Cliente QXVLA NULL NULL 8 Cliente QUHWH 10970 2019-03-24 ... 20 Cliente THHDP 10979 2019-03-26 20 Cliente THHDP 10968 2019-03-23 ​​20 Cliente THHDP 10895 2019-02-18 21 Cliente KIDPX NULL NULL 22 Cliente DTDMN NULL NULL 23 Cliente WVFAF NULL NULL 24 Cliente CYZTN 11050 2019-04 27 24 Cliente CYZTN 11001 2019-04-06 24 Cliente CYZTN 10993 2019-04-01 ... (213 filas afectadas)

El plan para esta consulta se muestra en la Figura 4.

Figura 4:Plan para Consulta 4

Como puede ver, el optimizador manejó la combinación como una combinación externa esta vez.

Esta es una consulta muy simple que utilicé con fines ilustrativos. Con consultas mucho más elaboradas y complejas, incluso los desarrolladores experimentados pueden tener dificultades para determinar si un predicado pertenece a la cláusula ON o a la cláusula WHERE. Lo que me facilita las cosas es simplemente preguntarme si el predicado es un predicado coincidente o de filtrado. Si es lo primero, pertenece a la cláusula ON; si es lo último, pertenece a la cláusula WHERE.

Contradicción de unión EXTERIOR-INTERIOR

Nuestro cuarto y último error es, en cierto modo, una variación del tercer error. Por lo general, ocurre en consultas de combinación múltiple en las que se mezclan tipos de combinación. Como ejemplo, suponga que necesita unir las tablas Customers, Orders, OrderDetails, Products y Suppliers para identificar pares de clientes y proveedores que tuvieron actividad conjunta. Escribe la siguiente consulta (llámela Consulta 5):

 SELECT DISTINCT C.custid, C.companyname AS cliente, S.supplierid, S.companyname AS proveedor FROM Ventas.Clientes AS C INNER JOIN Ventas.Pedidos AS O ON O.custid =C.custid INNER JOIN Ventas.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Esta consulta genera el siguiente resultado con 1236 filas:

 cusid cliente proveedorid proveedor ------- --------------- ----------- ---------- ----- 1 Cliente NRZBB 1 Proveedor SWRXU 1 Cliente NRZBB 3 Proveedor STUAZ 1 Cliente NRZBB 7 Proveedor GQRCV ... 21 Cliente KIDPX 24 Proveedor JNNES 21 Cliente KIDPX 25 Proveedor ERVYZ 21 Cliente KIDPX 28 Proveedor OAVQT 23 Cliente WVFAF 3 Proveedor STUAZ 23 Cliente WVFAF 7 Proveedor GQRCV 23 Cliente WVFAF 8 Proveedor BWGYE ... 56 Cliente QNIVZ 26 Proveedor ZWZDM 56 Cliente QNIVZ 28 Proveedor OAVQT 56 Cliente QNIVZ 29 Proveedor OGLRK 58 Cliente AHXHT 1 Proveedor SWRXU 58 Cliente AHXHT 5 Proveedor EQPNC 58 Cliente AHXHT 6 Proveedor QWUSF ... (1236 filas afectadas)

El plan para esta consulta se muestra en la Figura 5.

Figura 5:Plan para Consulta 5

Todas las uniones en el plan se procesan como uniones internas como era de esperar.

También puede observar en el plan que el optimizador aplicó la optimización de orden de unión. Con las uniones internas, el optimizador sabe que puede reorganizar el orden físico de las uniones de la forma que desee mientras conserva el significado de la consulta original, por lo que tiene mucha flexibilidad. Aquí, su optimización basada en costos dio como resultado el pedido:join(Clientes, join(Pedidos, join(join(Proveedores, Productos), Detalles del pedido))).

Suponga que obtiene un requisito para cambiar la consulta de modo que incluya clientes que no han realizado pedidos. Recuerde que actualmente tenemos dos de estos clientes (con ID 22 y 57), por lo que se supone que el resultado deseado tiene 1238 filas. Un error común en tal caso es cambiar la unión interna entre Clientes y Pedidos a una unión externa izquierda, pero dejar el resto de las uniones como internas, así:

 SELECT DISTINCT C.custid, C.companyname AS cliente, S.supplierid, S.companyname AS proveedor FROM Ventas.Clientes AS C LEFT OUTER JOIN Ventas.Pedidos AS O ON O.custid =C.custid INNER JOIN Ventas. Detalles del pedido AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Cuando una combinación externa izquierda es seguida posteriormente por combinaciones externas internas o externas derechas, y el predicado de combinación compara algo del lado no conservado de la combinación externa izquierda con algún otro elemento, el resultado del predicado es el valor lógico desconocido y el predicado externo original las filas se descartan. La combinación externa izquierda se convierte efectivamente en una combinación interna.

Como resultado, esta consulta genera el mismo resultado que la Consulta 5, devolviendo solo 1236 filas. También aquí el optimizador detecta la contradicción y convierte la combinación externa en una combinación interna, generando el mismo plan que se muestra anteriormente en la Figura 5.

Un intento común de corregir el error es hacer que todas las uniones se unan por la izquierda, así:

 SELECT DISTINCT C.custid, C.companyname AS cliente, S.supplierid, S.companyname AS proveedor FROM Ventas.Clientes AS C LEFT OUTER JOIN Ventas.Pedidos AS O ON O.custid =C.custid LEFT OUTER JOIN Ventas .OrderDetails AS OD ON OD.orderid =O.orderid LEFT OUTER JOIN Production.Products AS P ON P.productid =OD.productid LEFT OUTER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Esta consulta genera el siguiente resultado, que incluye a los clientes 22 y 57:

 cusid cliente proveedorid proveedor ------- --------------- ----------- ---------- ----- 1 Cliente NRZBB 1 Proveedor SWRXU 1 Cliente NRZBB 3 Proveedor STUAZ 1 Cliente NRZBB 7 Proveedor GQRCV ... 21 Cliente KIDPX 24 Proveedor JNNES 21 Cliente KIDPX 25 Proveedor ERVYZ 21 Cliente KIDPX 28 Proveedor OAVQT 22 Cliente DTDMN NULL NULL 23 Cliente WVFAF 3 Proveedor STUAZ 23 Cliente WVFAF 7 Proveedor GQRCV 23 Cliente WVFAF 8 Proveedor BWGYE ... 56 Cliente QNIVZ 26 Proveedor ZWZDM 56 Cliente QNIVZ 28 Proveedor OAVQT 56 Cliente QNIVZ 29 Proveedor OGLRK 57 Cliente WVAXS NULL NULL 58 Cliente AHXHT 1 Proveedor SWRXU 58 Cliente AHXHT 5 Proveedor EQPNC 58 Cliente AHXHT 6 Proveedor QWUSF ... (1238 filas affe cted)

Sin embargo, hay dos problemas con esta solución. Suponga que, además de Clientes, podría tener filas en otra tabla de la consulta sin filas coincidentes en una tabla posterior y que, en tal caso, no desea conservar esas filas externas. Por ejemplo, qué pasaría si en su entorno se permitiera crear un encabezado para un pedido y, en un momento posterior, llenarlo con líneas de pedido. Supongamos que, en tal caso, no se supone que la consulta devuelva encabezados de pedido vacíos. Aún así, se supone que la consulta devuelve clientes sin pedidos. Dado que la combinación entre Pedidos y Detalles del pedido es una combinación externa izquierda, esta consulta devolverá dichos pedidos vacíos, aunque no debería.

Otro problema es que cuando se utilizan uniones externas, se imponen más restricciones al optimizador en cuanto a las reorganizaciones que puede explorar como parte de su optimización de ordenación de uniones. El optimizador puede reorganizar la unión A LEFT OUTER JOIN B a B RIGHT OUTER JOIN A, pero esa es prácticamente la única reorganización que puede explorar. Con las uniones internas, el optimizador también puede reordenar las tablas más allá de simplemente cambiar de lado, por ejemplo, puede reordenar join(join(join(join(A, B), C), D), E)))) para unir(A, join(B, join(join(E, D), C))) como se muestra anteriormente en la Figura 5.

Si lo piensa, lo que realmente busca es unir a la izquierda a los Clientes con el resultado de las uniones internas entre el resto de las tablas. Obviamente, puede lograr esto con expresiones de tabla. Sin embargo, T-SQL admite otro truco. Lo que realmente determina el orden de las uniones lógicas no es exactamente el orden de las tablas en la cláusula FROM, sino el orden de las cláusulas ON. Sin embargo, para que la consulta sea válida, cada cláusula ON debe aparecer justo debajo de las dos unidades que está uniendo. Entonces, para considerar la unión entre los Clientes y el resto como el último, todo lo que necesita hacer es mover la cláusula ON que conecta a los Clientes y el resto para que aparezca en último lugar, así:

 SELECCIONE DISTINCT C.custid, C.companyname AS cliente, S.supplierid, S.companyname COMO proveedor FROM Ventas.Clientes AS C IZQUIERDA EXTERNA ÚNASE Ventas.Pedidos AS O -- mover desde aquí ------- ---------------- INNER JOIN Sales.OrderDetails AS OD -- ON OD.orderid =O.orderid -- INNER JOIN Production.Products AS P -- ON P.productid =OD .productid -- INNER JOIN Production.Suppliers AS S -- ON S.supplierid =P.supplierid -- ON O.custid =C.custid; -- <-- hasta aquí --

Ahora el orden lógico de unión es:leftjoin(Clientes, join(join(join(Pedidos, Detalles del pedido), Productos), Proveedores)). Esta vez, mantendrá a los clientes que no hayan realizado pedidos, pero no mantendrá los encabezados de los pedidos que no tengan líneas de pedido coincidentes. Además, le permite al optimizador una flexibilidad total de pedidos de unión en las uniones internas entre pedidos, detalles de pedidos, productos y proveedores.

El único inconveniente de esta sintaxis es la legibilidad. La buena noticia es que esto se puede arreglar fácilmente usando paréntesis, así (llame a esta Consulta 6):

 SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS proveedor FROM Sales.Customers AS C LEFT OUTER JOIN ( Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid ) ON O.custid =C.custid;

No confunda el uso de paréntesis aquí con una tabla derivada. Esta no es una tabla derivada, sino solo una forma de separar algunos de los operadores de la tabla en su propia unidad, para mayor claridad. El idioma realmente no necesita estos paréntesis, pero se recomiendan enfáticamente para mejorar la legibilidad.

El plan para esta consulta se muestra en la Figura 6.

Figura 6:Plan para Consulta 6

Observe que esta vez la unión entre Clientes y el resto se procesa como una unión externa y que el optimizador aplicó la optimización de orden de unión.

Conclusión

En este artículo cubrí cuatro errores clásicos relacionados con las uniones. When using outer joins, computing the COUNT(*) aggregate typically results in a bug. The best practice is to apply the aggregate to a non-NULLable column from the nonpreserved side of the join.

When joining multiple tables and involving aggregate calculations, if you apply the aggregates to a nonleaf table in the joins, it’s usually a bug resulting in double-dipping aggregates. The best practice is then to apply the aggregates within table expressions and joining the table expressions.

It’s common to confuse the meanings of the ON and WHERE clauses. With inner joins, they’re both filters, so it doesn’t really matter how you organize your predicates within these clauses. However, with outer joins the ON clause serves a matching role whereas the WHERE clause serves a filtering role. Understanding this helps you figure out how to organize your predicates within these clauses.

In multi-join queries, a left outer join that is subsequently followed by an inner join, or a right outer join, where you compare an element from the nonpreserved side of the join with others (other than the IS NULL test), the outer rows of the left outer join are discarded. To avoid this bug, you want to apply the left outer join last, and this can be achieved by shifting the ON clause that connects the preserved side of this join with the rest to appear last. Use parentheses for clarity even though they are not required.