El FULL OUTER JOIN completa la familia de los joins externos: conserva todas las filas de ambas tablas, casen o no. Es la unión de un LEFT JOIN y un RIGHT JOIN en una sola operación.
Es también, con diferencia, el JOIN que menos veces escribirás. Y por una razón interesante que esta lección explica: en una base de datos con integridad referencial bien declarada, como la de TiendaVerde, los huérfanos solo pueden existir en un lado. Un FULL OUTER JOIN entre dos tablas relacionadas por una clave foránea degenera casi siempre en un LEFT JOIN. Su verdadero terreno de juego está en otra parte: la conciliación de dos fuentes de datos independientes, donde ninguna de las dos manda sobre la otra.
Contenido
- La regla y el diagrama de conjuntos
FULL OUTER JOINsobre TiendaVerde: por qué solo aparecen huérfanos en un lado- El caso real: conciliar dos fuentes de datos
- El patrón "solo lo que no casa en ninguno de los dos lados"
- Emulación en motores que no lo soportan
- Soporte por motor y coste
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- La regla y el diagrama de conjuntos
flowchart LR
subgraph R[" "]
direction LR
A(("solo en la izquierda<br/>✅ con NULL a la derecha"))
I(("casan<br/>✅ resultado"))
B(("solo en la derecha<br/>✅ con NULL a la izquierda"))
end
style A fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
style I fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
style B fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
El cuadro completo de la familia, ya con los cuatro tipos:
| Tipo | Izquierda huérfana | Casan | Derecha huérfana | Filas del resultado |
|---|---|---|---|---|
INNER JOIN |
❌ | ✅ | ❌ | casan |
LEFT JOIN |
✅ | ✅ | ❌ | casan + huérfanas izq. |
RIGHT JOIN |
❌ | ✅ | ✅ | casan + huérfanas der. |
FULL OUTER JOIN |
✅ | ✅ | ✅ | casan + huérfanas izq. + huérfanas der. |
Sintaxis: FULL OUTER JOIN y FULL JOIN son lo mismo, porque OUTER es opcional igual que en LEFT y RIGHT. En este curso escribimos FULL OUTER JOIN completo, porque su rareza justifica ser explícito.
Y una propiedad que lo distingue de los otros dos externos: el FULL OUTER JOIN sí es conmutativo. A FULL OUTER JOIN B y B FULL OUTER JOIN A devuelven el mismo conjunto de filas, porque ambos lados reciben el mismo trato.
| Propiedad | INNER |
LEFT |
RIGHT |
FULL OUTER |
|---|---|---|---|---|
| Conmutativo | ✅ | ❌ | ❌ | ✅ |
FULL OUTER JOIN sobre TiendaVerde: por qué solo aparecen huérfanos en un lado
FULL OUTER JOIN sobre TiendaVerde: por qué solo aparecen huérfanos en un ladoProbemos el caso más evidente: catálogo frente a ventas.
SELECT p.id AS producto_id,
p.nombre AS producto,
lp.id AS linea_id,
lp.cantidad
FROM productos AS p
FULL OUTER JOIN lineas_pedido AS lp ON lp.producto_id = p.id
ORDER BY p.id, lp.id;50 filas. Exactamente las mismas que devolvía el LEFT JOIN de 03-03:
| Consulta | Filas |
|---|---|
productos INNER JOIN lineas_pedido |
47 |
productos LEFT JOIN lineas_pedido |
50 |
productos FULL OUTER JOIN lineas_pedido |
50 |
productos RIGHT JOIN lineas_pedido |
47 |
El FULL OUTER JOIN no ha aportado ni una fila sobre el LEFT JOIN. ¿Por qué?
Porque para que apareciera un huérfano por el lado derecho tendría que existir una línea de pedido cuyo producto_id no correspondiera a ningún producto. Y eso es precisamente lo que la clave foránea impide:
Dos restricciones actúan a la vez:
| Restricción | Qué impide |
|---|---|
REFERENCES productos(id) |
Insertar una línea con un producto_id que no exista en productos |
NOT NULL |
Insertar una línea sin producto_id |
ON DELETE RESTRICT |
Borrar un producto que tenga líneas, dejándolas huérfanas |
flowchart LR
A["¿Puede haber un producto<br/>sin líneas de venta?"] -->|"Sí: 13, 19 y 20"| B["huérfanos<br/>por la IZQUIERDA"]
C["¿Puede haber una línea<br/>sin producto?"] -->|"No: lo impide la FK"| D["huérfanos por la DERECHA:<br/>imposibles"]
De ahí sale una regla general muy útil:
Entre dos tablas unidas por una clave foránea
NOT NULLcon integridad referencial declarada, unFULL OUTER JOINes siempre equivalente a unLEFT JOINdesde la tabla padre. Escribirlo es redundante, y además paga un coste que no necesita.
Lo mismo ocurre con clientes y pedidos: pedidos.cliente_id es NOT NULL REFERENCES clientes(id), así que clientes FULL OUTER JOIN pedidos devuelve las mismas 23 filas que el LEFT JOIN.
El único caso de TiendaVerde en que el FULL OUTER sí aportaría algo es pedidos con empleados, porque empleado_id admite NULL:
- Huérfanos por la izquierda: los 10 pedidos web sin empleado.
- Huérfanos por la derecha: los 5 empleados sin pedidos.
- Total: 10 emparejamientos + 10 + 5 = 25 filas.
Es el único FULL OUTER JOIN con sentido en este esquema, y aun así sería más claro escribir dos consultas separadas: "pedidos sin comercial" y "empleados sin pedidos" responden a preguntas de negocio distintas, y mezclarlas en una tabla con nulos por los dos lados no ayuda a nadie.
- El caso real: conciliar dos fuentes de datos
El FULL OUTER JOIN brilla cuando las dos tablas no están unidas por una clave foránea: cuando son dos fuentes independientes que deberían coincidir y hay que averiguar en qué no coinciden. Ejemplos habituales:
| Conciliación | Fuente A | Fuente B | Qué se busca |
|---|---|---|---|
| Catálogo frente a ventas externas | Catálogo propio | Fichero de un marketplace | SKU que no existen, productos no publicados |
| Inventario frente a contabilidad | Recuento físico de almacén | Sistema contable | Diferencias de existencias |
| Nóminas frente a directorio | Sistema de RR. HH. | Directorio de usuarios | Altas y bajas no propagadas |
| Cobros frente a facturas | Extracto bancario | Facturas emitidas | Cobros sin factura, facturas sin cobro |
En todos ellos la clave es la misma: ninguna de las dos fuentes es la autoridad. Cada una puede tener filas que la otra no tiene, y el objetivo del informe es precisamente encontrarlas.
El escenario: ventas del marketplace
TiendaVerde ha empezado a vender también en un marketplace externo, que cada mes envía un fichero CSV con las unidades vendidas por SKU. Cargamos ese fichero en una tabla temporal:
Aviso:
ventas_marketplaceno forma parte del esquema de TiendaVerde. Es una tabla auxiliar creada solo para este ejemplo. No la uses en el resto de ejercicios del curso, y bórrala al terminar (o desconéctate: al serTEMP, desaparece con la sesión).
CREATE TEMP TABLE ventas_marketplace (
sku INTEGER,
unidades INTEGER
);
INSERT INTO ventas_marketplace (sku, unidades) VALUES
( 1, 14), ( 2, 9), ( 3, 6), ( 4, 11), ( 5, 22),
( 6, 4), ( 7, 7), ( 8, 3), ( 9, 12), (10, 5),
(11, 8), (12, 6), (14, 15), (15, 2), (16, 10),
(17, 4), (18, 19), (101, 7), (102, 3);19 filas. Fíjate en dos cosas: no hay ninguna clave foránea entre sku y productos.id —el fichero viene de otro sistema, no podemos imponerle restricciones—, y aparecen dos SKU extraños, el 101 y el 102.
La conciliación completa
SELECT p.id AS producto_id,
p.nombre AS producto,
vm.sku,
vm.unidades
FROM productos AS p
FULL OUTER JOIN ventas_marketplace AS vm ON vm.sku = p.id
ORDER BY COALESCE(p.id, vm.sku);| producto_id | producto | sku | unidades |
|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 1 | 14 |
| 2 | Arroz integral ecológico 1 kg | 2 | 9 |
| 3 | Miel de azahar cruda 500 g | 3 | 6 |
| 4 | Pasta de espelta 500 g | 4 | 11 |
| 5 | Tomate triturado ecológico 400 g | 5 | 22 |
| 6 | Crema facial de aloe vera 50 ml | 6 | 4 |
| 7 | Champú sólido de romero 80 g | 7 | 7 |
| 8 | Aceite corporal de almendras 200 ml | 8 | 3 |
| 9 | Bálsamo labial de caléndula 15 ml | 9 | 12 |
| 10 | Detergente ecológico concentrado 1 L | 10 | 5 |
| 11 | Estropajo vegetal de luffa (pack 3) | 11 | 8 |
| 12 | Bolsas reutilizables de algodón (pack 5) | 12 | 6 |
| 13 | Velas de cera de soja (pack 2) | (null) | (null) |
| 14 | Infusión de manzanilla ecológica 20 uds | 14 | 15 |
| 15 | Té verde matcha ceremonial 30 g | 15 | 2 |
| 16 | Kombucha de jengibre 750 ml | 16 | 10 |
| 17 | Zumo de naranja prensado en frío 1 L | 17 | 4 |
| 18 | Cepillo de dientes de bambú | 18 | 19 |
| 19 | Desodorante natural en barra 50 g | (null) | (null) |
| 20 | Cápsulas de espirulina 120 uds | (null) | (null) |
| (null) | (null) | 101 | 7 |
| (null) | (null) | 102 | 3 |
22 filas = 17 emparejamientos + 3 productos sin ventas en el marketplace + 2 SKU desconocidos.
Ahora sí hay huérfanos por los dos lados, y cada uno cuenta una historia distinta:
- Los productos 13, 19 y 20 no se han vendido en el marketplace. Coinciden con los que tampoco se venden en la tienda propia, lo que refuerza el diagnóstico: sin stock, descatalogado y con problema comercial.
- Los SKU 101 y 102 han generado ventas en el marketplace pero no existen en el catálogo. Eso es una alerta operativa seria: puede ser una referencia antigua con otra numeración, un error de mapeo entre sistemas o ventas que nadie está imputando a ningún producto.
Un detalle de escritura: el ORDER BY COALESCE(p.id, vm.sku) ordena por el identificador "venga de donde venga". Si ordenaras solo por p.id, las dos filas del marketplace tendrían NULL en esa columna y se irían al final (o al principio, según NULLS FIRST/LAST, como viste en 02-05). COALESCE devuelve el primer valor no nulo de la lista, y se estudia a fondo en 06-04.
- El patrón "solo lo que no casa en ninguno de los dos lados"
La conciliación completa está bien para revisar, pero lo que se envía a operaciones es solo la lista de discrepancias. Es el equivalente al anti-join de 03-03, ahora por partida doble:
SELECT p.id AS producto_id,
p.nombre AS producto,
vm.sku,
vm.unidades
FROM productos AS p
FULL OUTER JOIN ventas_marketplace AS vm ON vm.sku = p.id
WHERE p.id IS NULL
OR vm.sku IS NULL
ORDER BY COALESCE(p.id, vm.sku);| producto_id | producto | sku | unidades |
|---|---|---|---|
| 13 | Velas de cera de soja (pack 2) | (null) | (null) |
| 19 | Desodorante natural en barra 50 g | (null) | (null) |
| 20 | Cápsulas de espirulina 120 uds | (null) | (null) |
| (null) | (null) | 101 | 7 |
| (null) | (null) | 102 | 3 |
5 filas: las tres discrepancias del catálogo y las dos del fichero. Esto es un informe de conciliación de verdad, y en un sistema real llegaría por correo cada mañana.
En términos de conjuntos, este patrón devuelve la diferencia simétrica:
flowchart LR
subgraph R[" "]
direction LR
A(("solo catálogo<br/>✅"))
I(("casan<br/>❌ excluidas por el WHERE"))
B(("solo marketplace<br/>✅"))
end
style A fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
style I fill:#f8f8f8,stroke:#bbb,stroke-dasharray: 4 4
style B fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
Dos detalles importantes de escritura:
| Detalle | Por qué |
|---|---|
El operador es OR, no AND |
Con AND pedirías filas donde falten las dos claves a la vez, algo imposible: toda fila del resultado viene de al menos un lado. AND devuelve siempre 0 filas |
| Se comparan las claves de cada lado | p.id y vm.sku. Igual que en el anti-join simple, hay que elegir columnas que no puedan ser NULL de forma legítima en sus tablas de origen |
Y las tres variantes del filtro, según lo que quieras:
WHERE |
Devuelve | Filas aquí |
|---|---|---|
| (ninguno) | Conciliación completa | 22 |
p.id IS NULL OR vm.sku IS NULL |
Solo discrepancias (diferencia simétrica) | 5 |
vm.sku IS NULL |
Solo lo que está en el catálogo y no en el fichero | 3 |
p.id IS NULL |
Solo lo que está en el fichero y no en el catálogo | 2 |
- Emulación en motores que no lo soportan
MySQL y MariaDB no implementan FULL OUTER JOIN. La emulación clásica consiste en combinar un LEFT JOIN y un RIGHT JOIN con UNION:
-- Emulación de FULL OUTER JOIN en MySQL
SELECT p.id AS producto_id, p.nombre AS producto, vm.sku, vm.unidades
FROM productos AS p
LEFT JOIN ventas_marketplace AS vm ON vm.sku = p.id
UNION
SELECT p.id, p.nombre, vm.sku, vm.unidades
FROM productos AS p
RIGHT JOIN ventas_marketplace AS vm ON vm.sku = p.id;La lógica es directa:
flowchart TD
A["LEFT JOIN<br/>casan + huérfanos izquierda<br/>= 20 filas"] --> C["UNION<br/>apila y elimina duplicados"]
B["RIGHT JOIN<br/>casan + huérfanos derecha<br/>= 19 filas"] --> C
C --> D["22 filas<br/>= FULL OUTER JOIN"]
Las 17 filas que casan aparecen en ambas consultas. Por eso hay que usar UNION y no UNION ALL: UNION elimina los duplicados y deja 20 + 19 − 17 = 22 filas. Con UNION ALL obtendrías 39 filas, con las 17 coincidencias repetidas dos veces.
UNION, UNION ALL y sus reglas son el tema de la lección 03-07; aquí basta con saber que existe la emulación y por qué necesita eliminar duplicados.
Nota de dialecto: en MySQL la emulación tiene una vuelta de tuerca desagradable. Como
UNIONcompara filas completas, dos filas que difieran en cualquier columna se consideran distintas; si en tu conjunto de datos hubiera filas repetidas de forma legítima,UNIONlas colapsaría y perderías información. La alternativa robusta es hacerLEFT JOINcompletoUNION ALLcon solo los huérfanos de la derecha (RIGHT JOIN ... WHERE p.id IS NULL), que no genera solapamiento y no necesita deduplicar.
- Soporte por motor y coste
| Motor | FULL OUTER JOIN |
Nota |
|---|---|---|
| PostgreSQL | ✅ Sí | Desde versiones muy antiguas, sin restricciones |
| MySQL / MariaDB | ❌ No | Hay que emularlo con UNION de LEFT y RIGHT |
| SQLite | ✅ Sí, desde 3.39 (junio de 2022) | Igual que RIGHT JOIN |
| SQL Server | ✅ Sí | Sin restricciones |
| Oracle | ✅ Sí | La sintaxis antigua (+) no puede expresar un full outer |
Por qué se usa poco
Cuatro razones, en orden de importancia:
- La integridad referencial lo hace innecesario dentro de un mismo esquema. Es el argumento de la sección 2, y explica la mayoría de los casos.
- Casi siempre lo que se quiere es una de las dos mitades. "Productos sin ventas" o "ventas sin producto" son preguntas concretas que un
LEFT JOINcon anti-join responde mejor, y con un resultado más legible. - El resultado es incómodo de leer. Una tabla con nulos por los dos lados obliga a
COALESCEpara ordenar, para agrupar y para presentar. Cada columna clave existe por duplicado. - Es el
JOINmás caro. El motor debe recorrer ambas relaciones por completo y marcar los no emparejados a los dos lados: no puede detenerse antes ni descartar pronto. En PostgreSQL solo se implementa con hash join o merge join; no existe un plan denested looppara unFULL OUTER JOIN, lo que a veces obliga a materializar y ordenar ambos lados. Con tablas grandes se nota (módulo 8).
Regla práctica: antes de escribir un
FULL OUTER JOIN, pregúntate si de verdad necesitas los huérfanos de ambos lados en el mismo resultado. Nueve de cada diez veces la respuesta es no, y unLEFT JOINes más claro y más rápido. La décima vez —una conciliación real entre dos sistemas— es exactamente para lo que existe.
Errores Comunes y Consejos
- Usar
FULL OUTER JOINentre dos tablas unidas por una FKNOT NULL. No aporta ninguna fila sobre elLEFT JOINy cuesta más. La integridad referencial ya garantiza que no hay huérfanos del lado hijo. - Escribir
ANDen lugar deORen el filtro de discrepancias.WHERE p.id IS NULL AND vm.sku IS NULLdevuelve siempre 0 filas: ninguna fila del resultado puede faltar por los dos lados a la vez. - Ordenar por una sola de las dos claves. Las filas huérfanas del otro lado tienen esa columna a
NULLy se van al extremo del listado. UsaORDER BY COALESCE(a.clave, b.clave). - Olvidar que las columnas clave están duplicadas. En la conciliación tienes
p.idyvm.sku. Para presentar un identificador único hace faltaCOALESCE(p.id, vm.sku). - Emularlo con
UNION ALLen MySQL. Duplica todas las filas que casan: 39 en vez de 22 en el ejemplo de esta lección. - Suponer que existe en MySQL. No existe, y no está previsto. Si tu SQL debe ser portable, evítalo.
- Confundir "sin pareja" con "dato vacío". Igual que en 03-03: comprueba siempre contra una columna que no pueda ser
NULLlegítimamente en su tabla de origen. - Consejo: empieza por el
LEFT JOINy comprueba si necesitas más. Ejecuta elLEFT, cuenta filas, ejecuta elFULL OUTERy compara. Si el número no cambia, elFULL OUTERsobra. - Consejo: en una conciliación, añade una columna que diga de dónde viene cada fila. Con
CASE(módulo 6) puedes etiquetar cada fila como "solo catálogo", "solo fichero" o "coincide". El informe se vuelve autoexplicativo. - Consejo: guarda el patrón de discrepancias.
FULL OUTER JOIN ... WHERE a.clave IS NULL OR b.clave IS NULLes una plantilla que reutilizarás cada vez que dos sistemas deban cuadrar.
Ejercicios
Ejercicio 1
Ejecuta estas dos consultas y compara el número de filas:
SELECT c.id, c.nombre, pe.id AS pedido_id
FROM clientes AS c
LEFT JOIN pedidos AS pe ON pe.cliente_id = c.id;
SELECT c.id, c.nombre, pe.id AS pedido_id
FROM clientes AS c
FULL OUTER JOIN pedidos AS pe ON pe.cliente_id = c.id;- ¿Cuántas filas devuelve cada una?
- Explica el resultado en términos de las restricciones declaradas sobre
pedidos.cliente_id. - ¿Qué tendría que cambiar en el esquema para que las dos consultas dieran resultados distintos?
Ejercicio 2
Sobre la tabla temporal ventas_marketplace de la sección 3, escribe dos consultas separadas:
- Los productos del catálogo que no aparecen en el fichero del marketplace, con su nombre, precio y stock.
- Los SKU del fichero que no existen en el catálogo, con las unidades vendidas.
Escribe la primera con un LEFT JOIN y la segunda con un RIGHT JOIN, y después razona: ¿por qué en este caso resultan más útiles dos consultas separadas que el FULL OUTER JOIN de la sección 4?
Ejercicio 3
Tu empresa migra a MySQL una consulta de conciliación escrita en PostgreSQL:
SELECT p.id AS producto_id, p.nombre AS producto, vm.sku, vm.unidades
FROM productos AS p
FULL OUTER JOIN ventas_marketplace AS vm ON vm.sku = p.id
WHERE p.id IS NULL OR vm.sku IS NULL;- Reescríbela para MySQL usando
UNION. - ¿Podrías usar
UNION ALLen esta reescritura concreta? Razona la respuesta mirando qué devuelve cada mitad.
Soluciones
Solución 1
1. Ambas devuelven 23 filas.
2. La columna está declarada así:
Las tres partes actúan juntas:
REFERENCES clientes(id)impide insertar un pedido con uncliente_idinexistente.NOT NULLimpide insertar un pedido sin cliente.ON DELETE RESTRICTimpide borrar un cliente que tenga pedidos, que es la otra forma de generar huérfanos.
Conclusión: no puede existir ningún pedido huérfano, así que el lado derecho no aporta nada y el FULL OUTER JOIN degenera en el LEFT JOIN. Las 23 filas son las mismas: 20 pedidos + 3 clientes sin pedidos.
3. Bastaría con que cliente_id admitiera NULL —por ejemplo, para registrar pedidos de invitados sin cuenta—. Esos pedidos no casarían con ningún cliente y aparecerían como huérfanos por la derecha, solo visibles con FULL OUTER JOIN (o con RIGHT JOIN). Es exactamente la situación de pedidos.empleado_id, que sí admite nulos: pedidos FULL OUTER JOIN empleados devuelve 25 filas frente a las 20 del LEFT JOIN.
Solución 2
1. Productos del catálogo que no están en el fichero:
SELECT p.id,
p.nombre AS producto,
p.precio,
p.stock
FROM productos AS p
LEFT JOIN ventas_marketplace AS vm ON vm.sku = p.id
WHERE vm.sku IS NULL
ORDER BY p.id;| id | producto | precio | stock |
|---|---|---|---|
| 13 | Velas de cera de soja (pack 2) | 13.75 | 0 |
| 19 | Desodorante natural en barra 50 g | 7.80 | 75 |
| 20 | Cápsulas de espirulina 120 uds | 16.40 | 55 |
2. SKU del fichero que no existen en el catálogo:
SELECT vm.sku,
vm.unidades
FROM productos AS p
RIGHT JOIN ventas_marketplace AS vm ON vm.sku = p.id
WHERE p.id IS NULL
ORDER BY vm.sku;| sku | unidades |
|---|---|
| 101 | 7 |
| 102 | 3 |
Por qué dos consultas separadas son más útiles aquí:
| Motivo | Explicación |
|---|---|
| Columnas distintas | Un producto sin ventas interesa con su precio y su stock; un SKU desconocido interesa con las unidades vendidas. En una sola tabla habría que devolver la unión de ambos juegos de columnas, con la mitad a NULL en cada fila |
| Destinatarios distintos | La primera lista va a marketing; la segunda, al equipo de integraciones. Son dos incidencias con dos responsables |
| Urgencia distinta | Un producto sin ventas es una observación comercial; un SKU vendido que no existe en el catálogo es un error de datos que hay que corregir hoy |
| Legibilidad | Cada consulta tiene un significado único y se explica sola. El FULL OUTER obliga a leer los nulos para saber de qué tipo es cada fila |
El FULL OUTER JOIN sigue siendo útil para la visión de conjunto —el cuadro de 22 filas de la sección 3, para revisar de un vistazo—, pero el trabajo operativo se despacha mejor con dos consultas dirigidas.
Solución 3
1. Reescritura para MySQL:
-- Mitad izquierda: productos del catálogo sin SKU en el fichero
SELECT p.id AS producto_id, p.nombre AS producto, vm.sku, vm.unidades
FROM productos AS p
LEFT JOIN ventas_marketplace AS vm ON vm.sku = p.id
WHERE vm.sku IS NULL
UNION
-- Mitad derecha: SKU del fichero sin producto en el catálogo
SELECT p.id, p.nombre, vm.sku, vm.unidades
FROM productos AS p
RIGHT JOIN ventas_marketplace AS vm ON vm.sku = p.id
WHERE p.id IS NULL;Devuelve las mismas 5 filas que la versión PostgreSQL.
2. ¿Se puede usar UNION ALL? Sí, y además es preferible.
El razonamiento es el que importa: UNION (sin ALL) hace falta cuando las dos mitades pueden producir las mismas filas, y hay que eliminar los duplicados. Ese es el caso de la emulación general de la sección 5, donde ambos JOIN devuelven las 17 filas que casan.
Aquí no ocurre. Cada mitad lleva su propio WHERE que la restringe a los huérfanos de su lado:
| Mitad | Filtro | Devuelve | Solapamiento |
|---|---|---|---|
| Izquierda | vm.sku IS NULL |
3 filas: productos sin SKU | — |
| Derecha | p.id IS NULL |
2 filas: SKU sin producto | ninguno |
Ninguna fila puede cumplir las dos condiciones a la vez, así que los conjuntos son disjuntos y no hay nada que deduplicar. UNION ALL es más rápido porque se ahorra el paso de ordenación o de tabla hash que UNION necesita para detectar duplicados. Es un ejemplo perfecto de la regla que verás en 03-07: usa UNION ALL salvo que tengas una razón concreta para eliminar duplicados.
Conclusión
Cierras la familia de los joins externos:
- El
FULL OUTER JOINconserva las filas huérfanas de ambos lados: es unLEFTy unRIGHTa la vez. Es el único join externo conmutativo. - Sobre TiendaVerde casi nunca aporta nada, y sabes explicar por qué: la integridad referencial (
REFERENCES+NOT NULL+ON DELETE RESTRICT) impide que existan huérfanos del lado hijo.productos FULL OUTER JOIN lineas_pedidodevuelve las mismas 50 filas que elLEFT JOIN. - Su terreno real es la conciliación de dos fuentes independientes, donde ninguna manda sobre la otra: catálogo contra fichero de un marketplace, inventario contra contabilidad, extracto bancario contra facturas.
- Has construido esa conciliación completa (22 filas) y el informe de discrepancias con el patrón
WHERE a.clave IS NULL OR b.clave IS NULL(5 filas): tres productos que el marketplace no vende y dos SKU que no existen en el catálogo. Recuerda: el operador esOR, nuncaAND. - Sabes emularlo con
UNIONde unLEFT JOINy unRIGHT JOINpara MySQL, y por qué esa emulación necesita eliminar duplicados salvo que cada mitad esté restringida a sus propios huérfanos. - Y conoces su coste: no admite plan de nested loop, obliga a recorrer ambas relaciones enteras y produce un resultado que necesita
COALESCEpara casi todo. Úsalo cuando de verdad necesites los dos lados.
En la lección siguiente, SELF JOIN y CROSS JOIN, veremos los dos JOIN "raros" que más confunden a quien empieza. El SELF JOIN te permitirá recorrer por fin las dos relaciones reflexivas de TiendaVerde: la jerarquía de empleados con jefe_id —donde volverás a necesitar un LEFT JOIN para no perder a Rosa, la directora sin jefe— y la red de referidos de clientes. El CROSS JOIN, que hasta ahora solo ha aparecido como accidente, se convertirá en una herramienta deliberada para generar combinaciones completas.
Curso de SQL
Módulo 1: Introducción a SQL
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
