FULL OUTER JOIN en MySQL

A diferencia de otros SGBD como SQL Server, Oracle o PostgreSQL, MySQL no implementa la operación FULL OUTER JOIN (me refiero a la versión 8.0.32 e inferiores). Sí implementa LEFT OUTER JOIN y RIGHT OUTER JOIN.

Para realizar esta operación en MySQL hay que hacer la UNION de ambos OUTER JOIN laterales. Pero la explicación requiere aclarar algún matiz adicional que encontrarás descrito a continuación

Supongamos que disponemos de las siguientes tablas, con datos de población y superficie de ciudades españolas:

Poblacion
+---------+------------+
| ciudad  | habitantes |
+---------+------------+
| Logroño |     151113 |
| Bilbao  |     345821 |
+---------+------------+
Extension
+---------+------------+
| ciudad  | superficie |
+---------+------------+
| Logroño |         81 |
| Madrid  |        604 |
+---------+------------+

Queremos obtener un listado que incluya toda la información de ambas tablas, incluyendo todas las ciudades, tanto las que están en las dos tablas (que obtendríamos con un INNER JOIN), como las que están en solo una de ellas. Es decir, necesitamos un FULL OUTER JOIN como el siguiente:

select coalesce(P.ciudad,E.ciudad) ciudad, habitantes, superficie
from Poblacion P full join Extension E on P.ciudad=E.ciudad;
+---------+------------+------------+
| ciudad  | habitantes | superficie |
+---------+------------+------------+
| Logroño |     151113 |         81 |
| Bilbao  |     345821 |       NULL |
| Madrid  |       NULL |        604 |
+---------+------------+------------+

Pero esto no funciona en MySQL. En MySQL es necesario ejecutar un OUTER JOIN lateral unido con el OUTER JOIN lateral contrario:

MEJORABLE
select * 
from Poblacion P left join Extension E on P.ciudad=E.ciudad
union
select *
from Poblacion P right join Extension E on P.ciudad=E.ciudad;

Analicémoslo paso a paso. Como todo OUTER JOIN, el primero (left en este ejemplo) añade al resultado las ciudades que aparecen en ambas tablas (lo que sería el resultado de un INNER JOIN equivalente) y, además, las ciudades de la tabla izquierda que no han sido emparejadas en el INNER JOIN (las que solo están en la tabla de la izquierda), completando con valores nulos para los campos de la tabla derecha. Es decir, el siguiente resultado parcialparcial (las tuplas apareasen con una indicación numérica que se utilizará luego):

Listado 1 - resultado de la primera parte de la consulta (LEFT JOIN)
+-----------+------------+-----------+------------+
| P.ciudad  | habitantes | E.ciudad  | superficie |
+-----------+------------+-----------+------------+
| Logroño   |     151113 | Logroño   |         81 |  --> 1
| Bilbao    |     345821 | NULL      |       NULL |  --> 2
+-----------+------------+-----------+------------+

El segundo OUTER JOIN (right) añade al resultado las ciudades que aparecen en ambas tablas y las ciudades de la tabla derecha que no han sido emparejadas en el JOIN (las que solo están en la tabla de la derecha), con valores nulos para los campos de la tabla izquierda. Es decir, el siguiente resultado parcial:

Listado 2 - resultado de la segunda parte de la consulta (RIGHT JOIN)
+-----------+------------+-----------+------------+
| P.ciudad  | habitantes | E.ciudad  | superficie |
+-----------+------------+-----------+------------+
| Logroño   |     151113 | Logroño   |         81 |  --> 3
| NULL      |       NULL | Madrid    |        604 |  --> 4
+-----------+------------+-----------+------------+

La unión se encarga de quitar las tuplas repetidas (deja solo una de entre las numeradas 1 y 3), de modo que el resultado es casi el que buscábamos:

Listado 3
+-----------+------------+-----------+------------+
| P.ciudad  | habitantes | E.ciudad  | superficie |
+-----------+------------+-----------+------------+
| Logroño   |     151113 | Logroño   |         81 |
| Bilbao    |     345821 | NULL      |       NULL |
| NULL      |       NULL | Madrid    |        604 |
+-----------+------------+-----------+------------+

Solo falta seleccionar los campos adecuados para la ciudad en cada OUTER JOIN. Estos serán P.ciudad en el primer JOIN (el LEFT JOIN), y E.ciudad en el segundo:

MEJORABLE
select P.ciudad, habitantes, superficie 
from Poblacion P left join Extension E on P.ciudad=E.ciudad
union
select E.ciudad, habitantes, superficie
from Poblacion P right join Extension E on P.ciudad=E.ciudad;

Así el resultado será el buscado:

Listado 4
+---------+------------+------------+
| ciudad  | habitantes | superficie |
+---------+------------+------------+
| Logroño |     151113 |         81 |
| Bilbao  |     345821 |       NULL |
| Madrid  |       NULL |        604 |
+---------+------------+------------+

Pero aún no hemos acabado. La consulta se puede mejorar haciéndola más eficiente, evitando las tuplas repetidas en los dos OUTER JOIN ejecutados. Si así lo hacemos, podríamos usar union all, que es más ventajoso desde el punto de vista del rendimiento que union, porque no se tiene que ocupar de eliminar los duplicados del resultado.

Las tuplas repetidas (fíjate en la tupla 3 del anterior Listado 2) aparecen como consecuencia de la ejecución del OUTER JOIN de la segunda consulta, y son las correspondientes a las ciudades emparejables de ambas tablas. Estás tuplas aparecen en la ejecución de la primera fase (la de INNER JOIN) tanto del left como del RIGHT JOIN (por eso se repiten). Para excluirlas debemos añadir una condición extra en esa segunda consulta que seleccione solo las tuplas que han sido añadidas en la segunda fase del RIGHT JOIN, es decir, la que añade al resultado las tuplas que no se han conseguido emparejar ejecutando el INNER JOIN equivalente. La característica de estas tuplas (ver tupla 4 del Listado 2) es que tienen valor null en todos los campos correspondientes a la tabla hacia la que no se inclina el OUTER JOIN, en este ejemplo, la de la izquierda (Poblacion). Por ello, escogeremos uno de esos campos (por ej. P.ciudad) y añadiremos la siguiente condición: P.ciudad is null.

BIEN
select P.ciudad, habitantes, superficie
from Poblacion P left join Extension E on P.ciudad=E.ciudad
union all
select E.ciudad, null, superficie
from Poblacion P right join Extension E on P.ciudad=E.ciudad
where P.ciudad is null;

El resultado es el mismo, pero la ejecución es más eficiente.

No hay comentarios:

Publicar un comentario