JOINs y Subconsultas en PostgreSQL
Hasta ahora cada SELECT leía una sola tabla. Pero los datos reales viven repartidos: los clientes en una tabla, sus pedidos en otra. JOIN es cómo le pedís a PostgreSQL que las combine — y las subconsultas son cómo usás el resultado de una consulta dentro de otra.
👁 — vistas · Ing. Roy Carrasco, Facultad de Ingeniería de Sistemas · UAB
Scrollea para empezar — la consulta y su resultado se arman solos, a la derecha, mientras leés.
Dos tablas que no se hablan
La tienda guarda a sus clientes en una tabla y sus pedidos en otra. Cada pedido solo anota el cliente_id de quien compró — ni el nombre, ni la ciudad. Si alguien pide un reporte con el nombre del cliente junto a lo que compró, ninguna de las dos tablas alcanza por sí sola.
Así viven hoy, separadas →
Una sola tabla, sin JOIN todavía
Toda consulta empieza de a poco. Elegimos las columnas que nos importan y arrancamos desde clientes. Sin ningún JOIN, solo vemos lo que esa tabla sabe: nombre y ciudad. Nada de productos ni montos — ese dato vive del otro lado.
Solo lo que coincide en los dos lados
Un INNER JOIN conecta ambas tablas por la columna que comparten — cliente_id con id — y se queda únicamente con las filas donde hay coincidencia en ambos lados.
Mirá el resultado: Renata Flores desaparece. No tiene ningún pedido registrado, así que no hay nada que combinar con su fila.
No perder a nadie de la izquierda
¿Y si el reporte necesita mostrar a todos los clientes, incluso a los que todavía no compraron nada? Cambiamos INNER por LEFT JOIN.
Renata vuelve a aparecer, con producto y total en NULL — Postgres rellena con NULL lo que no encontró del lado derecho.
El filtro llega después del JOIN
El JOIN ya armó la tabla combinada; WHERE decide qué filas de esa combinación se quedan. Filtramos solo clientes de La Paz.
Iván (Cochabamba) y Esteban (Santa Cruz) salen del resultado. Renata se mantiene — su ciudad sí es La Paz, aunque no tenga pedidos.
Quedarnos con lo mejor
Ordenamos por el total del pedido de mayor a menor y nos quedamos con los tres primeros con LIMIT. Cinco cláusulas armadas una encima de la otra — y en ningún momento tuviste que releer la consulta entera.
Guardate esta idea: INNER es "solo lo que coincide en ambos lados"; LEFT es "todo lo de la izquierda, coincida o no". Faltan dos variantes.
Visto desde el otro lado
RIGHT JOIN es LEFT JOIN mirando la tabla de la derecha: conserva todo lo de pedidos, coincida o no. Mirá el pedido 107 — quedó con un cliente_id que ya no existe (un cliente borrado). Con RIGHT, esa fila igual aparece, con el nombre en NULL.
En la práctica casi nadie usa RIGHT: es más fácil reordenar las tablas y usar siempre LEFT.
No perder nada de ningún lado
FULL OUTER JOIN combina LEFT y RIGHT a la vez: Renata (sin pedidos) y el pedido 107 (sin cliente válido) aparecen los dos, cada uno con NULL en el lado que le falta.
PostgreSQL lo soporta de forma nativa — no todos los motores lo hacen.
cliente_id compartido-- todavía no escribimos nada
Practicá lo que viste
Antes de seguir a subconsultas, confirmá que los 4 tipos de JOIN quedaron claros.
Una tabla autor tiene 5 filas y libro tiene 8, pero 2 autores todavía no cargaron ningún libro. ¿Cuántas filas devuelve un INNER JOIN entre autor y libro por autor_id?
Sobre la consulta que armaste arriba, en su versión final con LEFT JOIN.
1. La cláusula que decide qué columnas trae el resultado es .
2. Si usaras INNER en vez de LEFT en esa misma consulta, la fila de Renata Flores .
Clasifica cada tipo de JOIN según qué filas conserva.
Cuando no necesitás combinar, solo filtrar
A veces la pregunta no pide columnas de las dos tablas — solo pide filtrar una tabla según una condición que depende de la otra. Ahí un JOIN es más trabajo del necesario: entra la subconsulta, un SELECT completo viviendo dentro de otro SELECT.
Un solo valor, donde iría cualquier valor suelto
Una subconsulta escalar devuelve exactamente un valor, y se usa donde iría cualquier número o texto. "El pedido con el total más alto" es directo: MAX(total) garantiza un único resultado.
Si la subconsulta devolviera más de una fila, PostgreSQL no sabría con cuál comparar — y lanza un error.
Filtrar contra una lista que sale de otra consulta
cliente_id IN (una lista de ids) — esa lista puede salir de otro SELECT. "Clientes con algún pedido mayor a Bs 300" no necesita traer ninguna columna de pedidos, solo filtrar clientes.
"¿Existe al menos una fila que cumpla esto?"
EXISTS es una subconsulta correlacionada: la de adentro hace referencia a la fila de afuera (c.id). No le importa cuántas coincidencias hay, ni sus valores — solo si existe alguna. Postgres se detiene apenas encuentra la primera.
La misma pregunta que ya resolviste con LEFT JOIN
NOT EXISTS invierte la condición: "clientes sin ningún pedido". Es otra forma de llegar a la fila de Renata que viste en el LEFT JOIN — sin necesidad de combinar columnas de las dos tablas.
-- todavía no escribimos nada
Practicá lo que viste
Emparejá cada forma de subconsulta con su uso, y detectá el error clásico de las escalares.
Une cada forma de subconsulta con la pregunta que mejor responde.
Ejecutás WHERE precio = (SELECT precio FROM producto WHERE activo = true); y la subconsulta devuelve 4 filas (4 productos activos). ¿Qué pasa?
Ejercicios de razonamiento
En estas preguntas no se te pide recordar sintaxis: se te da un escenario, y debés razonar cuál sería la respuesta correcta antes de comprobarla.
Querés un reporte con el nombre de cada cliente y, si tiene, la fecha de su pedido más reciente. Los clientes que nunca compraron deben aparecer igual, con la fecha en blanco.
¿Qué tipo de combinación necesitás, con clientes como tabla izquierda?
Necesitás la lista de nombres de los productos que nunca se vendieron — no te interesa ninguna columna de la tabla de ventas, solo el nombre del producto.
¿Cuál enfoque es el más directo?
Querés saber el nombre del cliente que hizo el pedido con el monto total más alto de toda la tabla.
Completa: SELECT c.cliente_nombre FROM cliente c JOIN pedido p ON p.cliente_id = c.cliente_id WHERE p.total =
;
Actividades de repaso
Un repaso integrador de todo el módulo. Cada actividad se corrige al instante; tu progreso se guarda automáticamente en este navegador.
¿Cuál es la diferencia central entre INNER JOIN y LEFT JOIN?
FULL OUTER JOIN produce el mismo resultado que combinar LEFT JOIN y RIGHT JOIN, sin dejar ninguna fila de ningún lado fuera.
¿Por qué WHERE cliente_id = NULL nunca sirve para filtrar clientes sin correo, mientras que WHERE correo IS NULL sí?
Necesitás los nombres de productos y, para cada uno, el nombre de su categoría (sin dejar afuera productos sin categoría asignada). ¿Qué usás?
¿Qué significa que EXISTS "se detiene en la primera coincidencia"?
Cheat Sheet
| Forma | Para qué sirve |
|---|---|
FROM a JOIN b ON a.x = b.y | INNER JOIN — solo filas con coincidencia en ambos lados. |
FROM a LEFT JOIN b ON ... | Conserva todo lo de a, con NULL donde no hay coincidencia. |
FROM a RIGHT JOIN b ON ... | Conserva todo lo de b (poco usado; se prefiere reordenar y usar LEFT). |
FROM a FULL OUTER JOIN b ON ... | Conserva todo de ambos lados. |
WHERE col = (SELECT ...) | Subconsulta escalar — debe devolver un único valor. |
WHERE col IN (SELECT ...) | Filtra contra una lista de valores que sale de otra consulta. |
WHERE EXISTS (SELECT 1 ... ) | Filtra según "existe al menos una fila que cumple esto". |
WHERE NOT EXISTS (SELECT 1 ...) | Filtra según "no existe ninguna fila que cumpla esto". |
¿Sabías que...?
📐 JOIN viene del álgebra relacional
Edgar F. Codd definió el modelo relacional en 1970 con operadores matemáticos como la unión, la diferencia y el producto cartesiano restringido — el JOIN de SQL es, en esencia, ese producto cartesiano filtrado por una condición de igualdad entre columnas.
⚙️ El motor no ejecuta el JOIN "fila por fila" como lo leés
Aunque escribas INNER JOIN pensando en comparar cada fila contra cada fila, PostgreSQL casi nunca lo hace así: elige entre varios algoritmos (nested loop, hash join, merge join) según el tamaño de las tablas y los índices disponibles — tema que vas a ver en la guía de índices y EXPLAIN.
🔄 EXISTS y JOIN pueden dar el mismo resultado, pero no siempre el mismo plan
Dos consultas escritas distinto (un JOIN con DISTINCT vs. un WHERE EXISTS) pueden devolver exactamente las mismas filas, pero PostgreSQL las ejecuta con estrategias distintas — por eso "cuál es más rápida" depende de los datos, no es una regla fija.