🧪 Taller de Laboratorio
vistas

JOINs y Subconsultas en Vivo: Datos Reales, Consultas Reales

Otra vez PostgreSQL de verdad, compilado a WebAssembly, corriendo enteramente en tu navegador. Esta vez vas a combinar dos tablas de siete formas distintas y ver, en cada una, exactamente qué fila aparece, cuál desaparece y cuál se rellena con NULL — antes de confiar en cualquiera de ellas.

RC
Ing. Roy Carrasco
Facultad de Ingeniería de Sistemas · UAB

Bienvenido

Este taller es la práctica de laboratorio de JOINs y subconsultas en PostgreSQL. No vuelve a explicar la teoría desde cero —asume que ya viste la guía— y en cambio te da un PostgreSQL real para escribir cada tipo de JOIN y cada forma de subconsulta, viendo exactamente qué filas devuelve el motor.

📎 Prerrequisito

Si todavía no viste la teoría, repasa primero JOINs y subconsultas antes de continuar. Este taller usa un esquema nuevo y más chico —solo clientes y pedidos— pensado para que cada tipo de combinación se vea clarísima, sin el ruido de un catálogo completo.

⚠️ Todo vive en la memoria de esta pestaña

Esta página corre su propia base de datos, independiente de cualquier otro taller que hayas abierto antes. Existe solo mientras esta pestaña siga abierta: si recargas la página, la cierras, o navegas a otra guía, se pierde todo y hay que empezar de nuevo desde "Prepara tu base de datos". Si te equivocás y querés reiniciar sin recargar, usa el botón "Reiniciar base de datos" de abajo.

✍️ Lo escribís vos

Cada bloque parte vacío. El enunciado te dice qué tenés que lograr — el código SQL lo escribís de cero.

🩻 Errores reales

Los mensajes de error que vas a ver son exactamente los que da PostgreSQL en producción.

👻 Una fila huérfana a propósito

Un pedido de este taller apunta a un cliente que no existe. Es intencional: es la única forma de ver de verdad para qué sirven RIGHT y FULL OUTER JOIN.

La base de datos está vacía. Baja a "Prepara tu base de datos" para empezar.

Zona de pruebas libres

Este bloque queda disponible durante toda la guía. Usalo en cualquier momento para probar una idea propia, revisar el estado de una tabla con SELECT * FROM tabla;, o experimentar con algo que se te ocurra a mitad de un ejercicio — sin desordenar los bloques guiados.

🧪 Escribe cualquier sentencia SQL

Prepara tu base de datos

Este bloque no es un ejercicio — ya viene resuelto. Crea dos tablas chicas, clientes y pedidos, con exactamente los mismos datos que viste en la guía de teoría. Ejecútalo tal cual, una sola vez, antes de seguir.

Qué hace este bloque

Crea clientes (5 filas) y pedidos (7 filas). Fijate que pedidos.cliente_id no tiene una FOREIGN KEY — es a propósito: el pedido 107 apunta a cliente_id = 9, que no existe en clientes. Con una clave foránea normal, ese INSERT habría fallado. Esa fila "huérfana" es la que vas a usar más adelante para ver la diferencia real entre INNER, LEFT, RIGHT y FULL OUTER JOIN.

🏗️ Ejecuta esta preparación tal cual
Confirma los datos

El resultado final debería mostrarte los 5 clientes, con Renata Flores entre ellos. Renata nunca aparece en pedidos — no compró nada. El resto de esta guía asume exactamente estos datos: si algo no coincide, usa "🔄 Reiniciar base de datos" y vuelve a ejecutar este bloque.

1. INNER JOIN

Solo las filas donde las dos tablas coinciden. Empezamos por acá porque es el más simple de los cuatro.

Enunciado — 1.1

Escribe un SELECT con INNER JOIN entre clientes (alias c) y pedidos (alias p), uniendo por p.cliente_id = c.id. Trae c.nombre, p.producto y p.total.

🗄️ Ejercicio 1.1 — INNER JOIN básico
Contá las filas

Deberían salir 6 filas, no 5 ni 7. Renata no aparece (no tiene pedidos) y el pedido 107 tampoco (su cliente_id no existe en clientes) — ninguno de los dos tiene con qué combinarse.

Enunciado — 1.2

Sobre la misma unión, agrega WHERE c.ciudad = 'La Paz' y ordena el resultado por p.total de mayor a menor.

🗄️ Ejercicio 1.2 — INNER JOIN con WHERE y ORDER BY

1.3 — Antes de ejecutar, predice: ¿cuántas filas devuelve un INNER JOIN completo entre clientes y pedidos, sin ningún WHERE? Después escribe SELECT COUNT(*) sobre ese mismo INNER JOIN para comprobarlo.

🗄️ Ejercicio 1.3 — Predice y comprueba con COUNT(*)

2. LEFT JOIN

Todo lo de la tabla de la izquierda, tenga o no coincidencia del otro lado.

Enunciado — 2.1

Reescribe el ejercicio 1.1 cambiando INNER JOIN por LEFT JOIN. Confirma que ahora Renata Flores aparece, con producto y total en NULL.

🗄️ Ejercicio 2.1 — LEFT JOIN básico
Enunciado — 2.2

Sobre ese mismo LEFT JOIN, agrega WHERE p.id IS NULL para quedarte solo con los clientes que no tienen ningún pedido.

🗄️ Ejercicio 2.2 — Clientes sin pedidos, con LEFT JOIN
Guardá este resultado

Debería devolver una sola fila: Renata Flores. Más adelante en este taller vas a llegar exactamente al mismo resultado con dos técnicas distintas —una subconsulta con NOT IN y otra con NOT EXISTS— para comparar las tres formas de responder la misma pregunta.

3. RIGHT JOIN

El espejo del LEFT JOIN: todo lo de la tabla de la derecha, tenga o no coincidencia del otro lado.

Enunciado — 3.1

Escribe clientes AS c RIGHT JOIN pedidos AS p ON p.cliente_id = c.id, trayendo c.nombre, p.id y p.producto. Confirma que el pedido 107 aparece con c.nombre en NULL.

🗄️ Ejercicio 3.1 — RIGHT JOIN básico
Enunciado — 3.2

En la práctica casi nadie usa RIGHT JOIN: es más común reordenar las tablas y usar siempre LEFT. Reescribe la consulta de 3.1 con pedidos AS p LEFT JOIN clientes AS c (las tablas invertidas) para llegar exactamente al mismo resultado.

🗄️ Ejercicio 3.2 — El mismo resultado, con LEFT JOIN invertido

4. FULL OUTER JOIN

LEFT y RIGHT al mismo tiempo: no se pierde nada de ningún lado.

Enunciado — 4.1

Escribe un FULL OUTER JOIN entre clientes y pedidos, trayendo c.nombre, p.id y p.producto. Confirma que aparecen tanto Renata Flores como el pedido 107, cada uno con NULL del lado que le falta.

🗄️ Ejercicio 4.1 — FULL OUTER JOIN básico
Enunciado — 4.2

Sobre ese mismo FULL OUTER JOIN, agrega WHERE c.id IS NULL OR p.id IS NULL para quedarte solo con las "filas problema": las que no calzan del todo de ningún lado.

🗄️ Ejercicio 4.2 — Solo las filas sin pareja
Exactamente dos filas

Debería devolver exactamente 2 filas: Renata (sin pedido) y el pedido 107 (sin cliente válido) — las dos "anomalías" de esta base de datos, una por cada lado del JOIN.

5. Subconsulta escalar

Un SELECT completo viviendo dentro de otro, donde iría cualquier valor suelto — siempre que devuelva exactamente uno.

Enunciado — 5.1

Encuentra el producto y el total del pedido más caro, usando WHERE total = (SELECT MAX(total) FROM pedidos).

🗄️ Ejercicio 5.1 — Subconsulta escalar con MAX

5.2 — Provocá el error a propósito: escribe WHERE total = (SELECT total FROM pedidos WHERE cliente_id = 1). Marisol Quispe (cliente_id = 1) tiene dos pedidos, así que la subconsulta va a devolver dos filas.

💥 Ejercicio 5.2 — Subconsulta escalar que devuelve dos filas
Leé el mensaje de error

PostgreSQL no elige ninguna de las dos filas por su cuenta: rechaza la consulta entera con algo como more than one row returned by a subquery used as an expression. Para "compará contra cualquiera de varios valores posibles" no se usa = — se usa IN, que viene en la próxima sección.

6. Subconsulta con IN

Filtrar contra una lista de valores que sale de otra consulta, en vez de una lista escrita a mano.

Enunciado — 6.1

Selecciona nombre de los clientes cuyo id esté en la lista de cliente_id de los pedidos con total > 300.

🗄️ Ejercicio 6.1 — IN con una subconsulta
Enunciado — 6.2

Ahora al revés: selecciona nombre y ciudad de los clientes cuyo id no esté en ninguna fila de pedidos.cliente_id, usando NOT IN.

🗄️ Ejercicio 6.2 — Clientes sin pedidos, con NOT IN
El mismo resultado que en 2.2

Debería devolver otra vez solo a Renata Flores — la misma pregunta que respondiste con LEFT JOIN + IS NULL, resuelta ahora sin combinar ninguna columna de las dos tablas.

7. EXISTS y NOT EXISTS

Subconsultas correlacionadas: la de adentro mira la fila de afuera, y a Postgres solo le importa si existe alguna coincidencia — no cuántas ni cuáles.

Enunciado — 7.1

Selecciona nombre de los clientes que tienen al menos un pedido, usando WHERE EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.id).

🗄️ Ejercicio 7.1 — EXISTS correlacionado
Enunciado — 7.2

Invierte la condición con NOT EXISTS para encontrar a los clientes que no tienen ningún pedido — la tercera forma de llegar al mismo resultado que en 2.2 y 6.2.

🗄️ Ejercicio 7.2 — NOT EXISTS correlacionado
Tres caminos, un mismo destino

LEFT JOIN + WHERE p.id IS NULL (2.2), NOT IN (6.2) y NOT EXISTS (7.2) te dieron exactamente la misma respuesta: solo Renata Flores. No hay una única forma "correcta" de resolver esta pregunta — hay tres, y elegir entre ellas depende del caso y, más adelante en el curso, del plan de ejecución que arme Postgres.

Reto final: un reporte completo

Sin reiniciar la base de datos, resuelve estos tres desafíos combinando JOINs y subconsultas.

Requisitos
  • 1. Con LEFT JOIN, trae nombre, ciudad, producto y total de todos los clientes de La Paz (con o sin pedidos), ordenados de mayor a menor total. Agrega NULLS LAST al ORDER BY para que las filas sin pedido queden al final en lugar de al principio.
  • 2. Con una subconsulta escalar, encuentra los pedidos con total mayor al promedio de todos los pedidos (SELECT AVG(total) FROM pedidos).
  • 3. Con EXISTS, lista los clientes que tienen al menos un pedido de más de Bs 500.
🏆 Resuelve aquí el reto

Repaso

Seis preguntas cortas sobre lo que acabas de observar en vivo. Tu progreso se guarda automáticamente en este navegador.

0 / 0
actividades correctas en toda la guía
Selección múltiple

En el ejercicio 1.3 comprobaste con COUNT(*) cuántas filas devuelve un INNER JOIN completo entre clientes y pedidos, sin ningún WHERE. ¿Cuántas fueron, y por qué?

Selección múltiple

En el ejercicio 3.2 reescribiste el RIGHT JOIN de 3.1 como un LEFT JOIN, y llegaste al mismo resultado. ¿Qué tuviste que cambiar?

Clasificación

Clasifica cada forma de subconsulta según cómo se comporta.

Subconsulta escalar (con =)
Subconsulta con IN
EXISTS

Completar

Sobre el requisito 1 del reto final: LEFT JOIN de clientes de La Paz con sus pedidos, ORDER BY total DESC NULLS LAST.

1. Sin agregar NULLS LAST, por defecto PostgreSQL ubica las filas con total en NULL (como la de Renata) .

2. Para que esas filas queden al final en lugar del principio, hay que agregar explícitamente .

Selección múltiple

En el ejercicio 5.2 escribiste total = (SELECT total FROM pedidos WHERE cliente_id = 1), y la subconsulta devolvió 2 filas (los pedidos 101 y 103, ambos de Marisol). ¿Qué pasó?

Emparejamiento

Une cada consulta con lo que hace, sobre la pregunta "¿qué clientes no tienen pedidos?".

Cheat Sheet del taller

FormaPara qué sirve
FROM a JOIN b ON a.x = b.yINNER 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".
... ORDER BY col DESC NULLS LASTOrdena de mayor a menor, dejando los NULL al final en vez del principio.
Situación que viviste en este tallerMensaje real de PostgreSQL
Ejercicio 5.2: una subconsulta escalar que devuelve más de una filamore than one row returned by a subquery used as an expression

¿Sabías que...?

👻 Esta fila huérfana pasa en producción, no solo en el taller

Sin una FOREIGN KEY que lo impida, nada evita que un pedido quede apuntando a un cliente ya borrado. Es exactamente el escenario que ON DELETE RESTRICT o ON DELETE CASCADE (guía de DDL) están pensados para prevenir — el pedido 107 de este taller es lo que pasa cuando esa protección no existe.

🔄 EXISTS y JOIN pueden dar el mismo resultado, pero no siempre el mismo plan

Las tres consultas del ejercicio 7.2 devuelven exactamente las mismas filas, pero PostgreSQL puede ejecutarlas con estrategias distintas por dentro — cuál es "más rápida" depende del tamaño de las tablas y de los índices, no es una regla fija.

🛑 NOT IN tiene una trampa que NOT EXISTS no tiene

Si la subconsulta de un NOT IN devolviera aunque sea un solo NULL, la condición completa deja de encontrar filas — un efecto secundario poco intuitivo de cómo SQL compara contra NULL. En este taller no pasa (cliente_id nunca es NULL en pedidos), pero es una razón real por la que muchos prefieren NOT EXISTS por costumbre.