Curso de MySQL
Consultas JOIN en MySQL: unir tablas relacionadas
Por Víctor Peña · Publicado el

Hola, ¿cómo están? Continuando con el curso de MySQL, llegamos a los JOIN. Aquí es donde el modelo relacional muestra para qué servía separar la información en tablas.
¡Empecemos!
El problema que resuelven
Nuestra tabla de citas guarda paciente_id y doctor_id, no los nombres:
SELECT * FROM citas;
id | paciente_id | doctor_id | fecha | costo
1 | 1 | 1 | 2026-04-10 | 250.00
Correcto para almacenar, inútil para mostrar. Nadie quiere ver «el paciente 1 con el doctor 1».
El JOIN combina las filas de varias tablas usando la relación entre ellas.
INNER JOIN
El más usado. Devuelve solo las filas que tienen coincidencia en ambas tablas.
SELECT
c.fecha,
c.hora,
p.nombre AS paciente,
p.apellido AS apellido_paciente,
c.costo
FROM citas c
INNER JOIN pacientes p ON c.paciente_id = p.id;
Tres partes:
FROM citas c— la tabla principal, con aliasc.INNER JOIN pacientes p— la tabla que unimos, con aliasp.ON c.paciente_id = p.id— la condición de unión: qué columna de una corresponde a cuál de la otra.
Esa condición casi siempre es clave foránea igual a clave primaria. Si la escribes al revés o con las columnas equivocadas, la consulta no falla: devuelve datos incorrectos.
La palabra INNER es opcional: escribir solo JOIN significa lo mismo.
Los alias son casi obligatorios
Sin alias, una consulta con tres tablas se vuelve ilegible:
-- Difícil de leer
SELECT citas.fecha, pacientes.nombre, doctores.nombre
FROM citas
JOIN pacientes ON citas.paciente_id = pacientes.id
JOIN doctores ON citas.doctor_id = doctores.id;
-- Mucho mejor
SELECT c.fecha, p.nombre, d.nombre
FROM citas c
JOIN pacientes p ON c.paciente_id = p.id
JOIN doctores d ON c.doctor_id = d.id;
Y hay un caso donde son imprescindibles: cuando dos tablas tienen columnas con el mismo nombre. Aquí tanto pacientes como doctores tienen nombre, así que hay que calificarlas y darles alias distintos en el resultado:
SELECT
c.fecha,
CONCAT(p.nombre, ' ', p.apellido) AS paciente,
CONCAT(d.nombre, ' ', d.apellido) AS doctor
FROM citas c
JOIN pacientes p ON c.paciente_id = p.id
JOIN doctores d ON c.doctor_id = d.id;
Unir varias tablas
Se encadenan tantos JOIN como haga falta:
SELECT
c.fecha,
c.hora,
CONCAT(p.nombre, ' ', p.apellido) AS paciente,
CONCAT(d.nombre, ' ', d.apellido) AS doctor,
e.nombre AS especialidad,
c.costo,
c.estado
FROM citas c
JOIN pacientes p ON c.paciente_id = p.id
JOIN doctores d ON c.doctor_id = d.id
JOIN especialidades e ON d.especialidad_id = e.id
WHERE c.fecha >= CURDATE()
ORDER BY c.fecha, c.hora;
Fíjate en la cadena: de citas a doctores, y de doctores a especialidades. Cada JOIN se apoya en una relación del diagrama que diseñamos.
LEFT JOIN
Devuelve todas las filas de la tabla izquierda, tengan o no coincidencia. Donde no hay correspondencia, rellena con NULL.
Esta es la diferencia clave, y se entiende mejor con el caso donde importa:
-- INNER: solo pacientes que tienen citas
SELECT p.nombre, c.fecha
FROM pacientes p
JOIN citas c ON p.id = c.paciente_id;
-- LEFT: TODOS los pacientes, con o sin citas
SELECT p.nombre, c.fecha
FROM pacientes p
LEFT JOIN citas c ON p.id = c.paciente_id;
Con INNER JOIN, un paciente que nunca pidió cita desaparece del resultado. Con LEFT JOIN aparece, con NULL en las columnas de la cita.
Encontrar registros sin relación
Un uso muy práctico del LEFT JOIN:
-- Pacientes que nunca tuvieron una cita
SELECT p.nombre, p.apellido
FROM pacientes p
LEFT JOIN citas c ON p.id = c.paciente_id
WHERE c.id IS NULL;
El WHERE c.id IS NULL se queda justamente con las filas que no encontraron pareja. Es el patrón estándar para buscar huérfanos: clientes sin pedidos, productos sin ventas, usuarios sin actividad.
RIGHT JOIN
Lo mismo que LEFT JOIN pero al revés: conserva todas las filas de la tabla derecha.
SELECT p.nombre, c.fecha
FROM citas c
RIGHT JOIN pacientes p ON c.paciente_id = p.id;
En la práctica casi no se usa. Cualquier RIGHT JOIN se puede escribir como LEFT JOIN invirtiendo el orden de las tablas, y así es más fácil de leer. Conócelo por si lo encuentras en código ajeno.
CROSS JOIN
Combina cada fila de una tabla con cada fila de la otra. Sin condición ON.
SELECT d.nombre, h.hora
FROM doctores d
CROSS JOIN horarios h;
Si hay 4 doctores y 8 horarios, devuelve 32 filas. Sirve para generar combinaciones —una agenda de turnos disponibles, por ejemplo—, pero rara vez es lo que quieres.
Cuidado: si olvidas el ON en un JOIN normal, obtienes un producto cartesiano accidental. Con dos tablas de mil filas cada una, eso es un millón de filas.
El diagrama mental
Una forma simple de recordarlo:
| Tipo | Devuelve |
|---|---|
INNER JOIN |
Solo lo que coincide en ambas |
LEFT JOIN |
Todo lo de la izquierda + lo que coincida |
RIGHT JOIN |
Todo lo de la derecha + lo que coincida |
CROSS JOIN |
Todas las combinaciones posibles |
JOIN con tablas intermedias
Para relaciones muchos a muchos hay que pasar por la tabla pivote:
SELECT
c.fecha,
CONCAT(p.nombre, ' ', p.apellido) AS paciente,
es.nombre AS estudio,
es.precio,
ce.resultado
FROM citas c
JOIN pacientes p ON c.paciente_id = p.id
JOIN cita_estudio ce ON c.id = ce.cita_id
JOIN estudios es ON ce.estudio_id = es.id
WHERE c.fecha >= '2026-04-01';
Son dos JOIN para una sola relación: de citas a la tabla intermedia, y de ahí a estudios. Es el patrón siempre que atravieses un muchos a muchos.
WHERE o condición del ON
Con LEFT JOIN hay una diferencia que confunde bastante:
-- Pierde el efecto del LEFT: se comporta como INNER
SELECT p.nombre, c.fecha
FROM pacientes p
LEFT JOIN citas c ON p.id = c.paciente_id
WHERE c.estado = 'programada';
-- Correcto: conserva todos los pacientes
SELECT p.nombre, c.fecha
FROM pacientes p
LEFT JOIN citas c ON p.id = c.paciente_id AND c.estado = 'programada';
La razón: el WHERE se aplica después de la unión, y como los pacientes sin citas tienen c.estado = NULL, la condición los descarta.
Poniendo la condición en el ON, se aplica durante la unión y el LEFT sigue funcionando.
Regla práctica: en un LEFT JOIN, las condiciones sobre la tabla de la derecha van en el ON, no en el WHERE.
Rendimiento
Dos cosas que conviene saber desde ahora:
Las columnas del ON deberían estar indexadas. Las claves primarias ya lo están; las foráneas también si declaraste la restricción. Sin índice, cada unión recorre la tabla entera.
Trae solo las columnas que uses. Un SELECT * con cuatro tablas devuelve decenas de columnas, muchas repetidas.
Lo veremos en detalle en las lecciones de optimización e índices.
Ejercicios
- Listar todas las citas con nombre de paciente, nombre de doctor y especialidad.
- Mostrar los doctores que no tienen ninguna cita asignada.
- Listar las citas atendidas de pacientes menores de 18 años.
- Mostrar todas las especialidades, incluidas las que no tienen doctores.
Para la segunda y la cuarta necesitas LEFT JOIN.
Errores comunes
- Olvidar el
ONy generar un producto cartesiano. - Condición de unión incorrecta: la consulta no falla, devuelve datos erróneos.
WHEREsobre la tabla derecha de unLEFT JOIN, anulando su efecto.- No usar alias con varias tablas.
SELECT *en consultas con múltiples uniones.
Para cerrar
Los JOIN son la razón de ser del modelo relacional: guardas la información una sola vez y la recompones al consultar.
Lo esencial: INNER para lo que coincide, LEFT cuando necesitas conservar todo un lado, y el truco de WHERE ... IS NULL para encontrar registros sin relación.
En la siguiente lección veremos GROUP BY, para agrupar y calcular totales.
Saludos y éxitos.
Norvic Software
Desarrollamos el software que tu empresa necesita
Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp