En la lección anterior aprendimos a interrogar una tabla. Pero las preguntas que de verdad le interesan a BiblioRed no caben en una tabla sola: "¿quién tiene ahora mismo el ejemplar EJ-3081?", "¿qué socios no han cogido nunca nada prestado?", "¿de qué autor es el libro que más se retrasa?". Las respuestas están repartidas entre socios, prestamos, ejemplares, libros y autores.
Ese reparto no es un defecto: es exactamente lo que decidimos en la lección 02-01 al separar la obra (libros) del objeto físico (ejemplares), y al no repetir el nombre del socio en cada préstamo. La información se guarda una sola vez, en el sitio que le corresponde, y se recompone cuando hace falta. La herramienta que la recompone es el JOIN, la reunión ⋈ del álgebra relacional.
Esta es la lección donde SQL deja de parecer un buscador de tablas y empieza a parecer un lenguaje de consulta de verdad. Es larga y densa; tómatela con calma y ejecuta cada ejemplo sobre tu biblioredb.
Contenido
- Por qué hay que recomponer los datos
- Producto cartesiano y
CROSS JOIN INNER JOIN: la reunión básicaLEFT JOINy las preguntas negativasRIGHT JOINyFULL OUTER JOIN- Resumen visual de los tipos de
JOIN SELF JOIN: unir una tabla consigo misma- Encadenar tres o más tablas
- Filtrar en
ONo filtrar enWHERE - Subconsultas escalares
- Subconsultas con
IN,EXISTSyNOT EXISTS - Subconsultas correlacionadas
- Tablas derivadas: subconsultas en
FROM - Expresiones de tabla común:
WITH - Operadores de conjunto:
UNION,INTERSECT,EXCEPT - Errores comunes y consejos
- Ejercicios
- Conclusión
- Por qué hay que recomponer los datos
Mira la tabla prestamos en crudo:
| prestamo_id | socio_id | ejemplar_id | fecha_prestamo |
|---|---|---|---|
| 1 | 14 | 2 | 2026-03-02 |
| 2 | 15 | 1 | 2026-03-05 |
| 3 | 16 | 5 | 2026-03-11 |
Para un ser humano esto no dice nada: 14, 2, 15, 1… son referencias. El nombre del socio está en socios, el código del ejemplar en ejemplares y el título en libros. La alternativa —guardar el nombre y el título dentro de cada préstamo— es justamente lo que hacía la hoja de cálculo, y ya vimos el resultado: redundancia, inconsistencia y errores de tecleo.
El trato del modelo relacional es este: se guarda sin repetir, y se paga un JOIN al consultar. Es un trato excelente, porque escribir bien ocurre una vez y leer mal ocurre para siempre.
- Producto cartesiano y
CROSS JOIN
CROSS JOINAntes de emparejar filas correctamente, hay que entender qué pasa si no las emparejas. El producto cartesiano × combina cada fila de una tabla con cada fila de la otra.
SELECT s.nombre AS sucursal, a.apellidos AS autor
FROM sucursales s
CROSS JOIN autores a
ORDER BY s.sucursal_id, a.autor_id;Primeras filas:
| sucursal | autor |
|---|---|
| Centro | Palma |
| Centro | Follett |
| Centro | Valcárcel |
| Centro | Barreda |
| … | … |
4 sucursales × 8 autores = 32 filas. Con socios (10) y libros (9) serían 90; con las tablas reales de BiblioRed, 12.000 × 8.000 = 96 millones.
¿Sirve para algo? Sí, en un caso concreto: generar todas las combinaciones posibles de dos conjuntos, por ejemplo para construir una rejilla de "cada sucursal × cada estado posible" que después se rellena con datos. Fuera de eso, un producto cartesiano en producción casi siempre es un accidente.
La forma antigua, y el accidente clásico
Antes de SQL-92 no existía la palabra JOIN: las tablas se listaban en el FROM separadas por comas y la condición de emparejamiento se escribía en el WHERE.
-- Sintaxis antigua: funciona, pero es peligrosa
SELECT p.prestamo_id, s.apellidos
FROM prestamos p, socios s
WHERE s.socio_id = p.socio_id;El peligro es evidente: si olvidas la condición del WHERE, obtienes un producto cartesiano en silencio. Doce préstamos por diez socios son 120 filas que parecen datos legítimos. Con tablas grandes, la consulta se cuelga.
Con la sintaxis moderna, JOIN ... ON, la condición está pegada a la unión y no se puede perder de vista. Usa siempre JOIN explícito.
INNER JOIN: la reunión básica
INNER JOIN: la reunión básicaINNER JOIN es un producto cartesiano seguido de un filtro: devuelve solo las parejas de filas que cumplen la condición. La palabra INNER es opcional (JOIN a secas significa INNER JOIN), pero escribirla deja claro que no es un LEFT.
SELECT p.prestamo_id,
s.nombre || ' ' || s.apellidos AS socio,
p.fecha_prestamo
FROM prestamos p
INNER JOIN socios s ON s.socio_id = p.socio_id
ORDER BY p.prestamo_id;| prestamo_id | socio | fecha_prestamo |
|---|---|---|
| 1 | Marta Alsina | 2026-03-02 |
| 2 | Iván Pereda | 2026-03-05 |
| 3 | Nuria Bastos | 2026-03-11 |
| 4 | Marta Alsina | 2026-04-06 |
| 5 | Lucía Vendrell | 2026-04-12 |
| 6 | Nuria Bastos | 2026-05-04 |
| 7 | Álvaro Ferrán | 2026-05-08 |
| 8 | Sonia Quiroga | 2026-05-19 |
| 9 | Marta Alsina | 2026-07-14 |
| 10 | Iván Pereda | 2026-07-18 |
| 11 | Diego Salom | 2026-07-21 |
| 12 | Pau Miralles | 2026-07-25 |
Doce filas: una por préstamo. Marta Alsina aparece tres veces porque tiene tres préstamos; eso no es duplicación, es la realidad.
Alias de tabla
p y s son alias de tabla. No son obligatorios, pero sí muy recomendables:
- Acortan las referencias:
s.apellidosen lugar desocios.apellidos. - Son imprescindibles cuando dos tablas tienen columnas con el mismo nombre. Si escribes
SELECT socio_id FROM prestamos JOIN socios ON ..., el gestor no sabe de cuál de las dos hablas:
- Son obligatorios en un
SELF JOIN(apartado 7).
Consejo de estilo: usa iniciales reconocibles (s socios, p prestamos, e ejemplares, l libros, a autores, su sucursales) y sé coherente en todo el proyecto. En este curso siempre usaremos esas mismas.
Combinar JOIN con WHERE
SELECT s.socio_id,
s.nombre || ' ' || s.apellidos AS socio,
p.fecha_devolucion_prevista
FROM prestamos p
INNER JOIN socios s ON s.socio_id = p.socio_id
WHERE p.fecha_devolucion IS NULL
ORDER BY p.fecha_devolucion_prevista;| socio_id | socio | fecha_devolucion_prevista |
|---|---|---|
| 14 | Marta Alsina | 2026-08-04 |
| 15 | Iván Pereda | 2026-08-08 |
| 17 | Diego Salom | 2026-08-11 |
| 19 | Pau Miralles | 2026-08-15 |
Los cuatro socios que tienen algo pendiente de devolver, con el plazo de cada uno.
USING y NATURAL JOIN
Cuando las columnas de ambas tablas se llaman exactamente igual, existe una abreviatura:
USING (socio_id) equivale a ON s.socio_id = p.socio_id, y además fusiona las dos columnas en una sola en el resultado. Es cómodo y nuestro esquema lo permite, porque nombramos las claves ajenas igual que las primarias.
Existe también NATURAL JOIN, que empareja automáticamente por todas las columnas de nombre coincidente:
Aquí ejemplares y libros comparten libro_id… pero si algún día alguien añade a ambas una columna observaciones, el NATURAL JOIN empezará a emparejar también por ella y la consulta cambiará de significado sin que nadie la haya tocado. Es magia implícita: evítala. ON explícito o, como mucho, USING.
LEFT JOIN y las preguntas negativas
LEFT JOIN y las preguntas negativasINNER JOIN descarta lo que no empareja. A veces eso es justo lo que no quieres.
-- ¿Cuántos préstamos tiene cada socio? Con INNER JOIN, los socios
-- sin préstamos desaparecen del listado.
SELECT s.socio_id, s.apellidos, p.prestamo_id
FROM socios s
INNER JOIN prestamos p ON p.socio_id = s.socio_id;Devuelve 12 filas y solo aparecen 8 socios: Ramón Etxebarri (13) y Elena Roig (20) se han esfumado.
LEFT JOIN (abreviatura de LEFT OUTER JOIN) conserva todas las filas de la tabla izquierda; cuando no hay pareja a la derecha, rellena esas columnas con NULL.
SELECT s.socio_id, s.apellidos, p.prestamo_id, p.fecha_prestamo
FROM socios s
LEFT JOIN prestamos p ON p.socio_id = s.socio_id
ORDER BY s.socio_id, p.prestamo_id;| socio_id | apellidos | prestamo_id | fecha_prestamo |
|---|---|---|---|
| 11 | Ferrán | 7 | 2026-05-08 |
| 12 | Quiroga | 8 | 2026-05-19 |
| 13 | Etxebarri | (NULL) | (NULL) |
| 14 | Alsina | 1 | 2026-03-02 |
| 14 | Alsina | 4 | 2026-04-06 |
| 14 | Alsina | 9 | 2026-07-14 |
| 15 | Pereda | 2 | 2026-03-05 |
| 15 | Pereda | 10 | 2026-07-18 |
| 16 | Bastos | 3 | 2026-03-11 |
| 16 | Bastos | 6 | 2026-05-04 |
| 17 | Salom | 11 | 2026-07-21 |
| 18 | Vendrell | 5 | 2026-04-12 |
| 19 | Miralles | 12 | 2026-07-25 |
| 20 | Roig | (NULL) | (NULL) |
14 filas: los 12 préstamos más las dos filas "vacías" de Etxebarri y Roig.
El patrón anti-join: encontrar lo que NO existe
Y aquí llega uno de los patrones más útiles de todo SQL. Si las filas sin pareja son las que tienen NULL en las columnas de la derecha, basta con filtrarlas:
SELECT s.socio_id, s.nombre, s.apellidos, s.fecha_alta
FROM socios s
LEFT JOIN prestamos p ON p.socio_id = s.socio_id
WHERE p.prestamo_id IS NULL
ORDER BY s.socio_id;| socio_id | nombre | apellidos | fecha_alta |
|---|---|---|---|
| 13 | Ramón | Etxebarri | 2020-11-14 |
| 20 | Elena | Roig | 2024-10-01 |
Los dos socios que nunca han tomado nada prestado. Es la diferencia − del álgebra relacional, implementada con un LEFT JOIN.
Detalle crucial: la columna que compruebas con IS NULL debe ser una que nunca pueda ser nula en la tabla derecha —la clave primaria es la elección segura—. Si comprobaras WHERE p.fecha_devolucion IS NULL obtendrías otra cosa completamente distinta (los préstamos abiertos, más los socios sin préstamos).
Otro caso real de BiblioRed: los ejemplares que nunca se han prestado, candidatos a expurgo.
SELECT e.codigo, l.titulo, e.estado, e.fecha_adquisicion
FROM ejemplares e
JOIN libros l ON l.libro_id = e.libro_id
LEFT JOIN prestamos p ON p.ejemplar_id = e.ejemplar_id
WHERE p.prestamo_id IS NULL
ORDER BY e.codigo;| codigo | titulo | estado | fecha_adquisicion |
|---|---|---|---|
| EJ-3083 | El mapa del tiempo | disponible | 2021-06-01 |
| EJ-3088 | Álgebra para impacientes | reparacion | 2020-01-15 |
| EJ-3091 | El invierno de los pájaros | disponible | 2022-04-27 |
| EJ-3093 | Manual de jardinería urbana | disponible | 2023-09-11 |
| EJ-3094 | Manual de jardinería urbana | baja | 2023-09-11 |
| EJ-3095 | Memoria del Ensanche (1904) | disponible | 2003-01-15 |
Seis ejemplares que llevan en la estantería sin salir nunca. Justo el tipo de informe que la hoja de cálculo no podía producir.
RIGHT JOIN y FULL OUTER JOIN
RIGHT JOIN y FULL OUTER JOINRIGHT JOIN es la imagen especular de LEFT JOIN: conserva todas las filas de la tabla derecha.
SELECT a.apellidos AS autor, l.titulo
FROM libros l
RIGHT JOIN autores a ON a.autor_id = l.autor_id
ORDER BY a.apellidos, l.titulo;| autor | titulo |
|---|---|
| Barreda | Álgebra para impacientes |
| Barreda | Manual de jardinería urbana |
| Escolá | (NULL) |
| Follett | Los pilares de la Tierra |
| Lemos | El invierno de los pájaros |
| Ordóñez | Rutas del delta |
| Palma | El mapa del tiempo |
| Sorrentino | Cuadernos de Ravena |
| Valcárcel | La casa de las mareas |
Nueve filas: los ocho autores, con Óscar Barreda repetido por sus dos obras y Marina Escolá con NULL porque aún no tiene ninguna. Observa que "Memoria del Ensanche (1904)" no aparece: es el libro sin autor catalogado, y RIGHT JOIN conserva los autores, no los libros.
Toda consulta RIGHT JOIN se puede reescribir como LEFT JOIN invirtiendo las tablas, y esa es la forma que verás en el 95 % del código profesional:
Consejo: quédate con LEFT JOIN. Leer una consulta larga en la que unos JOIN van hacia la izquierda y otros hacia la derecha es innecesariamente difícil.
FULL OUTER JOIN
Conserva las filas sin pareja de ambos lados:
SELECT a.apellidos AS autor, l.titulo
FROM autores a
FULL OUTER JOIN libros l ON l.autor_id = a.autor_id
ORDER BY a.apellidos NULLS LAST, l.titulo;| autor | titulo |
|---|---|
| Barreda | Álgebra para impacientes |
| Barreda | Manual de jardinería urbana |
| Escolá | (NULL) |
| Follett | Los pilares de la Tierra |
| Lemos | El invierno de los pájaros |
| Ordóñez | Rutas del delta |
| Palma | El mapa del tiempo |
| Sorrentino | Cuadernos de Ravena |
| Valcárcel | La casa de las mareas |
| (NULL) | Memoria del Ensanche (1904) |
Diez filas: aparece tanto la autora sin libros como el libro sin autor. Es la consulta de auditoría por excelencia: enseña los dos lados descosidos de una vez.
Aviso sobre SQLite
RIGHT JOIN y FULL OUTER JOIN no existieron en SQLite hasta la versión 3.39 (junio de 2022). Si tu sqlite3 es anterior, esas dos consultas darán error de sintaxis. Comprueba tu versión con SELECT sqlite_version();. Soluciones:
- Reescribir el
RIGHT JOINcomoLEFT JOINcon las tablas invertidas (siempre posible). - Simular el
FULL OUTER JOINcon dosLEFT JOINunidos porUNION(apartado 15).
LEFT JOIN e INNER JOIN, en cambio, funcionan en SQLite desde siempre.
- Resumen visual de los tipos de
JOIN
JOINImagina dos tablas mínimas emparejadas por una clave:
- IZQ con claves
1, 2, 3 - DER con claves
2, 3, 4
Tipo de JOIN |
Filas que devuelve | Claves del resultado | Nº de filas |
|---|---|---|---|
INNER JOIN |
Solo las que emparejan | 2, 3 | 2 |
LEFT JOIN |
Todas las de la izquierda | 1 (con NULL), 2, 3 |
3 |
RIGHT JOIN |
Todas las de la derecha | 2, 3, 4 (con NULL) |
3 |
FULL OUTER JOIN |
Todas las de ambos lados | 1, 2, 3, 4 | 4 |
CROSS JOIN |
Todas las combinaciones | 3 × 3 parejas | 9 |
LEFT JOIN + IS NULL |
Solo las de la izquierda sin pareja | 1 | 1 |
Y el árbol de decisión que conviene tener en la cabeza al escribir una consulta:
flowchart TD
A["¿Qué filas quiero en el resultado?"] --> B{"¿Necesito filas<br/>sin correspondencia?"}
B -->|No: solo las emparejadas| C["INNER JOIN"]
B -->|Sí| D{"¿De qué lado?"}
D -->|"Solo de la tabla principal<br/>(la del FROM)"| E["LEFT JOIN"]
D -->|Solo de la secundaria| F["RIGHT JOIN<br/><i>mejor: dale la vuelta<br/>y usa LEFT JOIN</i>"]
D -->|De los dos lados| G["FULL OUTER JOIN"]
E --> H{"¿Quiero EXCLUSIVAMENTE<br/>las que no emparejan?"}
H -->|Sí| I["LEFT JOIN + WHERE clave_derecha IS NULL<br/><i>(anti-join)</i>"]
H -->|No| J["LEFT JOIN a secas"]
B -->|"Quiero todas las combinaciones<br/>posibles, sin emparejar"| K["CROSS JOIN"]
SELF JOIN: unir una tabla consigo misma
SELF JOIN: unir una tabla consigo mismaNo es un tipo distinto de JOIN: es un INNER o LEFT JOIN normal en el que las dos tablas son la misma. Sirve para comparar filas de una tabla entre sí, y por eso los alias son obligatorios: hay que poder distinguir las dos "copias".
Pregunta: ¿qué parejas de socios están dados de alta en la misma sucursal?
SELECT a.apellidos AS socio_a,
b.apellidos AS socio_b,
a.sucursal_id
FROM socios a
INNER JOIN socios b
ON b.sucursal_id = a.sucursal_id
AND b.socio_id > a.socio_id
ORDER BY a.sucursal_id, a.apellidos, b.apellidos;| socio_a | socio_b | sucursal_id |
|---|---|---|
| Bastos | Roig | 1 |
| Etxebarri | Bastos | 1 |
| Etxebarri | Roig | 1 |
| Ferrán | Bastos | 1 |
| Ferrán | Etxebarri | 1 |
| Ferrán | Roig | 1 |
| Alsina | Miralles | 2 |
| Alsina | Pereda | 2 |
| Pereda | Miralles | 2 |
| Quiroga | Salom | 3 |
Diez parejas. La condición b.socio_id > a.socio_id hace dos cosas a la vez y es el truco que hay que memorizar:
- Evita emparejar a cada socio consigo mismo (que sería
a.socio_id = b.socio_id). - Evita el duplicado simétrico: si sale (Ferrán, Roig), no sale también (Roig, Ferrán).
Sin esa condición obtendrías 4×4 + 3×3 + 2×2 + 1×1 = 30 filas en lugar de 10.
Otro SELF JOIN útil en BiblioRed: otras obras del mismo autor.
SELECT l1.titulo AS libro, l2.titulo AS otra_obra_del_mismo_autor
FROM libros l1
INNER JOIN libros l2 ON l2.autor_id = l1.autor_id
AND l2.libro_id <> l1.libro_id
ORDER BY l1.titulo;| libro | otra_obra_del_mismo_autor |
|---|---|
| Álgebra para impacientes | Manual de jardinería urbana |
| Manual de jardinería urbana | Álgebra para impacientes |
Aquí sí queremos las dos direcciones (para poder recomendar desde cualquiera de los dos libros), por eso usamos <> en lugar de >. Óscar Barreda es el único autor con dos obras en el fondo.
El uso canónico del SELF JOIN en el mundo real son las jerarquías: una tabla empleados con una columna jefe_id que apunta a la propia tabla. BiblioRed no tiene ninguna, pero el mecanismo es idéntico.
- Encadenar tres o más tablas
Los JOIN se encadenan de arriba abajo: el resultado del primero se une con la tabla siguiente, y así sucesivamente. La consulta estrella de BiblioRed recorre cinco tablas.
flowchart LR
S["socios"] --> P["prestamos"]
P --> E["ejemplares"]
E --> L["libros"]
L --> A["autores"]
SELECT s.nombre || ' ' || s.apellidos AS socio,
e.codigo AS ejemplar,
l.titulo,
a.apellidos AS autor,
p.fecha_prestamo,
p.fecha_devolucion_prevista
FROM prestamos p
INNER JOIN socios s ON s.socio_id = p.socio_id
INNER JOIN ejemplares e ON e.ejemplar_id = p.ejemplar_id
INNER JOIN libros l ON l.libro_id = e.libro_id
LEFT JOIN autores a ON a.autor_id = l.autor_id
WHERE p.fecha_devolucion IS NULL
ORDER BY p.fecha_prestamo;| socio | ejemplar | titulo | autor | fecha_prestamo | fecha_devolucion_prevista |
|---|---|---|---|---|---|
| Marta Alsina | EJ-3081 | El mapa del tiempo | Palma | 2026-07-14 | 2026-08-04 |
| Iván Pereda | EJ-3084 | Los pilares de la Tierra | Follett | 2026-07-18 | 2026-08-08 |
| Diego Salom | EJ-3086 | La casa de las mareas | Valcárcel | 2026-07-21 | 2026-08-11 |
| Pau Miralles | EJ-3090 | El invierno de los pájaros | Lemos | 2026-07-25 | 2026-08-15 |
Este es el listado que el bibliotecario quiere ver cada mañana. Y ya podemos responder a la pregunta del principio: quien tiene ahora mismo EJ-3081 es Marta Alsina, y debe devolverlo el 4 de agosto de 2026.
Tres decisiones de esa consulta merecen comentario:
- Empezamos por
prestamos, no porsocios. Cuando encadenas varias tablas, arrancar por la tabla "central" —la que tiene las claves ajenas hacia las demás— hace la consulta mucho más natural de leer. autoresse une conLEFT JOIN. ¿Por qué? Porquelibros.autor_idadmiteNULL(recuerda "Memoria del Ensanche"). Con unINNER JOIN, si alguien prestara ese ejemplar, el préstamo desaparecería del informe matinal sin que nadie se enterara. Es el error silencioso más frecuente al encadenar tablas: unINNER JOINsobre una clave ajena opcional pierde filas.- El orden en que escribes los
JOINno determina el orden de ejecución. El optimizador (lección 01-04) decide por su cuenta. Tú escribes para que se lea bien; él ejecuta para que corra rápido.
- Filtrar en
ON o filtrar en WHERE
ON o filtrar en WHEREEsta distinción es sutil, se pregunta en todas las entrevistas y provoca resultados incorrectos a diario. Con INNER JOIN da exactamente igual dónde pongas la condición. Con LEFT JOIN, cambia el resultado por completo.
La razón está en el orden de evaluación:
ONse aplica mientras se emparejan las filas: decide qué es pareja y qué no.WHEREse aplica después de construida la unión: descarta filas del resultado ya montado, incluidas las filas rellenas deNULLque elLEFT JOINacababa de conservar.
Pregunta: "lista de todos los socios, indicando qué préstamos han hecho a partir de julio de 2026".
Con la condición en ON
SELECT s.socio_id, s.apellidos, p.prestamo_id, p.fecha_prestamo
FROM socios s
LEFT JOIN prestamos p
ON p.socio_id = s.socio_id
AND p.fecha_prestamo >= '2026-07-01'
ORDER BY s.socio_id;| socio_id | apellidos | prestamo_id | fecha_prestamo |
|---|---|---|---|
| 11 | Ferrán | (NULL) | (NULL) |
| 12 | Quiroga | (NULL) | (NULL) |
| 13 | Etxebarri | (NULL) | (NULL) |
| 14 | Alsina | 9 | 2026-07-14 |
| 15 | Pereda | 10 | 2026-07-18 |
| 16 | Bastos | (NULL) | (NULL) |
| 17 | Salom | 11 | 2026-07-21 |
| 18 | Vendrell | (NULL) | (NULL) |
| 19 | Miralles | 12 | 2026-07-25 |
| 20 | Roig | (NULL) | (NULL) |
Diez filas: están todos los socios, que es lo que pedía la pregunta. Los que no tienen préstamos en julio salen con NULL.
Con la condición en WHERE
SELECT s.socio_id, s.apellidos, p.prestamo_id, p.fecha_prestamo
FROM socios s
LEFT JOIN prestamos p ON p.socio_id = s.socio_id
WHERE p.fecha_prestamo >= '2026-07-01'
ORDER BY s.socio_id;| socio_id | apellidos | prestamo_id | fecha_prestamo |
|---|---|---|---|
| 14 | Alsina | 9 | 2026-07-14 |
| 15 | Pereda | 10 | 2026-07-18 |
| 17 | Salom | 11 | 2026-07-21 |
| 19 | Miralles | 12 | 2026-07-25 |
Cuatro filas. El WHERE ha eliminado todas las filas con p.fecha_prestamo a NULL, porque NULL >= '2026-07-01' es DESCONOCIDO. El LEFT JOIN se ha convertido, de hecho, en un INNER JOIN.
| Dónde va la condición | Efecto en INNER JOIN |
Efecto en LEFT JOIN |
|---|---|---|
En ON |
Idéntico | Filtra qué se empareja; conserva todas las filas de la izquierda |
En WHERE |
Idéntico | Filtra el resultado; anula el efecto del LEFT |
Regla práctica: en un LEFT JOIN, las condiciones sobre la tabla derecha van en el ON; las condiciones sobre la tabla izquierda van en el WHERE. La única excepción es el patrón anti-join WHERE clave_derecha IS NULL, donde quieres deliberadamente ese efecto.
- Subconsultas escalares
Una subconsulta es un SELECT dentro de otro SELECT, entre paréntesis. La variedad más simple es la escalar: devuelve exactamente una fila y una columna, es decir, un valor suelto, y por tanto se puede usar en cualquier sitio donde encaje un valor.
En el WHERE
-- Libros publicados después de "El mapa del tiempo"
SELECT titulo, anio_publicacion
FROM libros
WHERE anio_publicacion > (SELECT anio_publicacion FROM libros WHERE libro_id = 331)
ORDER BY anio_publicacion;| titulo | anio_publicacion |
|---|---|
| Cuadernos de Ravena | 2012 |
| La casa de las mareas | 2015 |
| Rutas del delta | 2017 |
| Álgebra para impacientes | 2019 |
| El invierno de los pájaros | 2021 |
| Manual de jardinería urbana | 2023 |
La subconsulta se ejecuta una vez, devuelve 2008, y la consulta externa lo usa como si lo hubieras escrito a mano. La ventaja es que no tienes que saber el valor de antemano: si mañana se corrige el año de ese libro, la consulta sigue siendo correcta.
Si una subconsulta escalar devuelve más de una fila, el gestor da error:
SELECT titulo FROM libros
WHERE anio_publicacion > (SELECT anio_publicacion FROM libros WHERE editorial = 'Editorial Andana');Si devuelve cero filas, en cambio, no hay error: el resultado es NULL, y la comparación entera pasa a DESCONOCIDO, así que la consulta externa devuelve cero filas… en silencio. Otra aplicación de la lógica de tres valores.
En la lista del SELECT
SELECT e.codigo,
e.estado,
(SELECT l.titulo FROM libros l WHERE l.libro_id = e.libro_id) AS titulo
FROM ejemplares e
WHERE e.sucursal_id = 4
ORDER BY e.codigo;| codigo | estado | titulo |
|---|---|---|
| EJ-3088 | reparacion | Álgebra para impacientes |
| EJ-3092 | disponible | Rutas del delta |
Esto es equivalente a un LEFT JOIN con libros. Como norma general, prefiere el JOIN: es más legible, más flexible (puedes traer varias columnas de la tabla unida) y el optimizador suele tratarlo mejor. La subconsulta en el SELECT se reserva para cuando necesitas un único dato calculado y el JOIN complicaría la consulta.
- Subconsultas con
IN, EXISTS y NOT EXISTS
IN, EXISTS y NOT EXISTSCuando la subconsulta devuelve varias filas, no se puede comparar con =, pero sí preguntar por pertenencia o por existencia.
IN
-- Socios que tienen algún préstamo abierto
SELECT socio_id, nombre, apellidos
FROM socios
WHERE socio_id IN (SELECT socio_id FROM prestamos WHERE fecha_devolucion IS NULL)
ORDER BY socio_id;| socio_id | nombre | apellidos |
|---|---|---|
| 14 | Marta | Alsina |
| 15 | Iván | Pereda |
| 17 | Diego | Salom |
| 19 | Pau | Miralles |
Compáralo con la versión JOIN. Con JOIN habría que añadir DISTINCT para no repetir a los socios con varios préstamos abiertos; con IN no hace falta, porque la pertenencia a un conjunto es sí o no. Esa es su ventaja principal.
NOT IN y la trampa del NULL
-- Libros que nadie ha reservado nunca
SELECT libro_id, titulo
FROM libros
WHERE libro_id NOT IN (SELECT libro_id FROM reservas)
ORDER BY libro_id;| libro_id | titulo |
|---|---|
| 334 | Álgebra para impacientes |
| 335 | Cuadernos de Ravena |
| 337 | Rutas del delta |
| 338 | Manual de jardinería urbana |
| 339 | Memoria del Ensanche (1904) |
Funciona porque reservas.libro_id es NOT NULL. Ahora la misma idea sobre una columna que sí admite nulos:
-- Autores sin ninguna obra en el fondo
SELECT autor_id, apellidos
FROM autores
WHERE autor_id NOT IN (SELECT autor_id FROM libros);Cero filas, cuando la respuesta correcta es "Marina Escolá". El motivo lo vimos en 02-01: la subconsulta devuelve {1, 2, 3, 4, 5, 6, 7, NULL} (el NULL es el de "Memoria del Ensanche"), y 8 NOT IN (..., NULL) se traduce en 8 <> 1 AND ... AND 8 <> NULL, cuyo último término es DESCONOCIDO. Y VERDADERO AND DESCONOCIDO es DESCONOCIDO, que no pasa el filtro. Con un solo NULL en la lista, NOT IN no devuelve jamás ninguna fila.
Las tres soluciones, de peor a mejor:
-- 1) Filtrar los NULL a mano: funciona, pero hay que acordarse siempre
SELECT autor_id, apellidos FROM autores
WHERE autor_id NOT IN (SELECT autor_id FROM libros WHERE autor_id IS NOT NULL);
-- 2) Anti-join con LEFT JOIN
SELECT a.autor_id, a.apellidos FROM autores a
LEFT JOIN libros l ON l.autor_id = a.autor_id
WHERE l.libro_id IS NULL;
-- 3) NOT EXISTS: inmune al problema por construcción
SELECT a.autor_id, a.apellidos FROM autores a
WHERE NOT EXISTS (SELECT 1 FROM libros l WHERE l.autor_id = a.autor_id);Las tres devuelven ahora:
| autor_id | apellidos |
|---|---|
| 8 | Escolá |
EXISTS y NOT EXISTS
EXISTS no compara valores: pregunta si la subconsulta devuelve al menos una fila. Devuelve TRUE o FALSE, nunca DESCONOCIDO, y por eso es inmune a la trampa anterior.
-- Socios con algún préstamo abierto (la misma pregunta que con IN)
SELECT s.socio_id, s.nombre, s.apellidos
FROM socios s
WHERE EXISTS (SELECT 1
FROM prestamos p
WHERE p.socio_id = s.socio_id
AND p.fecha_devolucion IS NULL)
ORDER BY s.socio_id;| socio_id | nombre | apellidos |
|---|---|---|
| 14 | Marta | Alsina |
| 15 | Iván | Pereda |
| 17 | Diego | Salom |
| 19 | Pau | Miralles |
El SELECT 1 es una convención: como solo importa si hay filas, no qué filas, se pone una constante. SELECT * funcionaría igual y el gestor lo optimiza idénticamente.
-- Socios que nunca han tomado nada prestado
SELECT s.socio_id, s.nombre, s.apellidos
FROM socios s
WHERE NOT EXISTS (SELECT 1 FROM prestamos p WHERE p.socio_id = s.socio_id)
ORDER BY s.socio_id;| socio_id | nombre | apellidos |
|---|---|---|
| 13 | Ramón | Etxebarri |
| 20 | Elena | Roig |
Mismo resultado que el anti-join del apartado 4. Tres formas de expresar la misma pregunta:
| Forma | Legibilidad | Riesgo con NULL |
Cuándo usarla |
|---|---|---|---|
LEFT JOIN ... IS NULL |
Media | Ninguno (si compruebas la clave primaria) | Cuando además necesitas columnas de la tabla derecha |
NOT IN (subconsulta) |
Alta | Alto | Solo si la columna es NOT NULL |
NOT EXISTS |
Alta | Ninguno | La opción por defecto |
- Subconsultas correlacionadas
Las subconsultas de EXISTS que acabas de ver tienen una particularidad: mencionan una columna de la consulta externa (s.socio_id). Eso las convierte en correlacionadas: no se pueden ejecutar por sí solas, porque dependen de la fila que se esté evaluando en cada momento.
| Subconsulta independiente | Subconsulta correlacionada | |
|---|---|---|
| ¿Se puede ejecutar aparte? | Sí | No |
| ¿Cuántas veces se evalúa? | Una | Conceptualmente, una por fila externa |
| Ejemplo | WHERE libro_id IN (SELECT libro_id FROM reservas) |
WHERE EXISTS (SELECT 1 FROM reservas r WHERE r.libro_id = l.libro_id) |
Un ejemplo de BiblioRed que responde a una pregunta genuinamente difícil de otro modo: ¿qué ejemplares pertenecen a un libro que tiene alguna reserva activa? (son los que hay que apartar en cuanto vuelvan al mostrador).
SELECT e.codigo, e.estado, e.sucursal_id, l.titulo
FROM ejemplares e
INNER JOIN libros l ON l.libro_id = e.libro_id
WHERE EXISTS (SELECT 1
FROM reservas r
WHERE r.libro_id = e.libro_id
AND r.estado = 'activa')
ORDER BY e.codigo;| codigo | estado | sucursal_id | titulo |
|---|---|---|---|
| EJ-3081 | prestado | 2 | El mapa del tiempo |
| EJ-3082 | disponible | 1 | El mapa del tiempo |
| EJ-3083 | disponible | 3 | El mapa del tiempo |
| EJ-3084 | prestado | 1 | Los pilares de la Tierra |
| EJ-3085 | disponible | 2 | Los pilares de la Tierra |
Cinco ejemplares en alerta: los tres de "El mapa del tiempo" (dos reservas activas) y los dos de "Los pilares de la Tierra" (una).
Sobre el rendimiento: la descripción "se evalúa una vez por fila externa" es el modelo mental, no lo que ocurre necesariamente. Los optimizadores modernos suelen transformar una subconsulta correlacionada en un JOIN internamente. Aun así, sobre tablas grandes conviene medirlo; en la lección 06-03 aprenderás a comprobarlo con EXPLAIN.
- Tablas derivadas: subconsultas en
FROM
FROMUna subconsulta también puede ocupar el lugar de una tabla en el FROM. Se llama tabla derivada y, como cualquier tabla, necesita un alias.
SELECT su.nombre AS sucursal,
ab.codigo,
ab.fecha_prestamo
FROM (SELECT p.prestamo_id, p.fecha_prestamo, e.codigo, e.sucursal_id
FROM prestamos p
INNER JOIN ejemplares e ON e.ejemplar_id = p.ejemplar_id
WHERE p.fecha_devolucion IS NULL) AS ab
INNER JOIN sucursales su ON su.sucursal_id = ab.sucursal_id
ORDER BY su.nombre, ab.fecha_prestamo;| sucursal | codigo | fecha_prestamo |
|---|---|---|
| Centro | EJ-3084 | 2026-07-18 |
| Centro | EJ-3086 | 2026-07-21 |
| Norte | EJ-3081 | 2026-07-14 |
| Norte | EJ-3090 | 2026-07-25 |
Los cuatro préstamos abiertos, repartidos entre las sucursales donde vive cada ejemplar. La tabla derivada ab (de "abiertos") existe solo durante la consulta.
Las tablas derivadas resuelven casos en los que necesitas trabajar sobre un resultado intermedio, sobre todo cuando ese intermedio incluye agregados —cosa que veremos en la lección 02-05—. Su inconveniente es la legibilidad: si anidas dos o tres, la consulta se vuelve un laberinto de paréntesis que hay que leer de dentro afuera.
- Expresiones de tabla común:
WITH
WITHUna CTE (Common Table Expression, expresión de tabla común) es una tabla derivada a la que se le da nombre antes de usarla. Misma potencia, legibilidad incomparablemente mejor.
WITH abiertos AS (
SELECT p.prestamo_id, p.fecha_prestamo, e.codigo, e.sucursal_id
FROM prestamos p
INNER JOIN ejemplares e ON e.ejemplar_id = p.ejemplar_id
WHERE p.fecha_devolucion IS NULL
)
SELECT su.nombre AS sucursal,
ab.codigo,
ab.fecha_prestamo
FROM abiertos ab
INNER JOIN sucursales su ON su.sucursal_id = ab.sucursal_id
ORDER BY su.nombre, ab.fecha_prestamo;Devuelve exactamente lo mismo que el apartado anterior, pero ahora la consulta se lee de arriba abajo, como un procedimiento: "primero calculo los abiertos, después los cruzo con las sucursales".
Varias CTE encadenadas
Se separan por comas, y cada una puede usar las anteriores:
WITH abiertos AS (
SELECT p.prestamo_id, p.socio_id, p.fecha_prestamo, e.codigo
FROM prestamos p
INNER JOIN ejemplares e ON e.ejemplar_id = p.ejemplar_id
WHERE p.fecha_devolucion IS NULL
),
socios_norte AS (
SELECT socio_id, nombre, apellidos
FROM socios
WHERE sucursal_id = 2
)
SELECT sn.nombre || ' ' || sn.apellidos AS socio,
ab.codigo,
ab.fecha_prestamo
FROM socios_norte sn
INNER JOIN abiertos ab ON ab.socio_id = sn.socio_id
ORDER BY ab.fecha_prestamo;| socio | codigo | fecha_prestamo |
|---|---|---|
| Marta Alsina | EJ-3081 | 2026-07-14 |
| Iván Pereda | EJ-3084 | 2026-07-18 |
| Pau Miralles | EJ-3090 | 2026-07-25 |
Tres de los cuatro préstamos abiertos corresponden a socios de la sucursal Norte; el cuarto es de Diego Salom, dado de alta en Sur.
Ventajas de las CTE:
- Legibilidad: cada bloque tiene nombre y propósito.
- Reutilización: una CTE se puede referenciar varias veces en la consulta principal, mientras que una tabla derivada habría que escribirla dos veces.
- Depuración: puedes ejecutar el contenido de la CTE por separado para ver qué produce.
Notas de dialecto:
WITHes estándar y funciona en PostgreSQL (desde 8.4) y SQLite (desde 3.8.3).- Existe
WITH RECURSIVEpara recorrer jerarquías y grafos (árboles de categorías, listas de materiales). Es potente y queda fuera del alcance de este curso introductorio. - Históricamente PostgreSQL trataba cada CTE como una barrera de optimización; desde la versión 12 las integra en la consulta principal salvo que escribas
MATERIALIZED. Si trabajas con una versión anterior y notas lentitud, esa puede ser la causa.
- Operadores de conjunto:
UNION, INTERSECT, EXCEPT
UNION, INTERSECT, EXCEPTLos JOIN combinan tablas a lo ancho (añaden columnas). Los operadores de conjunto las combinan a lo alto (apilan filas). Son la unión ∪, la intersección ∩ y la diferencia − del álgebra relacional.
Para usarlos, las dos consultas deben ser compatibles: mismo número de columnas y tipos compatibles, en el mismo orden. Los nombres de columna los pone la primera consulta.
UNION y UNION ALL
-- Socios "en movimiento": con préstamo abierto o con reserva activa
SELECT socio_id FROM prestamos WHERE fecha_devolucion IS NULL
UNION
SELECT socio_id FROM reservas WHERE estado = 'activa'
ORDER BY socio_id;| socio_id |
|---|
| 14 |
| 15 |
| 16 |
| 17 |
| 18 |
| 19 |
Seis socios. La primera consulta devuelve {14, 15, 17, 19} y la segunda {16, 18, 15}; Iván Pereda (15) está en las dos y aparece una sola vez, porque UNION elimina duplicados.
SELECT socio_id FROM prestamos WHERE fecha_devolucion IS NULL
UNION ALL
SELECT socio_id FROM reservas WHERE estado = 'activa'
ORDER BY socio_id;| socio_id |
|---|
| 14 |
| 15 |
| 15 |
| 16 |
| 17 |
| 18 |
| 19 |
Siete filas: UNION ALL no elimina duplicados. Y precisamente por eso es más rápido: no tiene que ordenar ni comparar nada. Regla práctica: usa UNION ALL salvo que necesites la deduplicación. Mucha gente escribe UNION por costumbre y paga el coste sin motivo.
INTERSECT
-- Socios que tienen préstamo abierto Y ADEMÁS reserva activa
SELECT socio_id FROM prestamos WHERE fecha_devolucion IS NULL
INTERSECT
SELECT socio_id FROM reservas WHERE estado = 'activa';| socio_id |
|---|
| 15 |
Iván Pereda: tiene "Los pilares de la Tierra" en préstamo y "El mapa del tiempo" reservado.
EXCEPT
-- Socios con préstamo abierto pero SIN ninguna reserva activa
SELECT socio_id FROM prestamos WHERE fecha_devolucion IS NULL
EXCEPT
SELECT socio_id FROM reservas WHERE estado = 'activa'
ORDER BY socio_id;| socio_id |
|---|
| 14 |
| 17 |
| 19 |
EXCEPT no es simétrico: A EXCEPT B no es lo mismo que B EXCEPT A. Si invirtieras el orden obtendrías {16, 18}, los socios con reserva activa y sin préstamos abiertos.
Detalles a tener en cuenta
INTERSECTyEXCEPTtambién eliminan duplicados por defecto; existenINTERSECT ALLyEXCEPT ALL.- El
ORDER BYva al final, una sola vez, y ordena el resultado combinado. No puedes poner uno en cada rama (salvo entre paréntesis conLIMIT). - En Oracle,
EXCEPTse llamaMINUS. - SQLite admite
UNION,UNION ALL,INTERSECTyEXCEPTdesde siempre; es su punto fuerte frente a losJOINexternos.
Un uso muy práctico en SQLite antiguo: simular un FULL OUTER JOIN.
SELECT a.apellidos, l.titulo FROM autores a LEFT JOIN libros l ON l.autor_id = a.autor_id
UNION
SELECT a.apellidos, l.titulo FROM libros l LEFT JOIN autores a ON a.autor_id = l.autor_id;Las diez filas del apartado 5, sin necesitar FULL OUTER JOIN.
Errores Comunes y Consejos
- Olvidar la condición de unión. Con la sintaxis antigua de comas produce un producto cartesiano silencioso. Usa siempre
JOIN ... ON. - Usar
INNER JOINsobre una clave ajena que admiteNULL. Pierdes filas sin previo aviso. Si la columna puede ser nula,LEFT JOIN. - Poner la condición de la tabla derecha en el
WHEREde unLEFT JOIN. Lo convierte enINNER JOINy te quedas sin las filas que querías conservar. Va en elON. - Comprobar
IS NULLsobre la columna equivocada en un anti-join. Usa siempre la clave primaria de la tabla derecha, que nunca puede ser nula por sí misma. NOT INcon una subconsulta que puede devolverNULL. Cero filas, siempre, en silencio. UsaNOT EXISTS.- Confundir "más filas de las esperadas" con un error del
JOIN. Si un socio tiene tres préstamos, aparecerá tres veces: eso es correcto. El problema surge al contar (COUNT) sobre ese resultado, y lo veremos en la lección 02-05. - Encadenar
JOINsin alias. Con cinco tablas y columnas homónimas, una consulta sin alias es ilegible e incluso ambigua para el gestor. - Usar
NATURAL JOIN. Empareja por nombres coincidentes y cambia de significado cuando alguien añade una columna. - Anidar tablas derivadas de tres niveles. Conviértelas en CTE con
WITH: mismo resultado, la mitad de tiempo para entenderla. - Consejo: cuando una consulta multitabla devuelva algo raro, quítale cláusulas hasta que funcione. Ejecuta primero el
JOINa pelo conSELECT *, mira cuántas filas salen y ve añadiendo condiciones de una en una. - Consejo: escribe siempre la condición de unión en el orden
ON tabla_nueva.columna = tabla_ya_presente.columna. Es una convención menor, pero al leer una consulta de cinco tablas se agradece muchísimo.
Ejercicios
Ejercicio 1: Reuniones básicas
- Lista los ejemplares con el título del libro al que pertenecen y el nombre de su sucursal. Ordena por sucursal y código.
- Muestra las reservas activas con el nombre del socio y el título del libro reservado.
- Lista todos los libros con el apellido de su autor, incluidos los que no tienen autor catalogado.
- Muestra los préstamos devueltos con retraso, indicando socio, título y las dos fechas.
Ejercicio 2: Preguntas negativas
- ¿Qué libros no tienen ningún ejemplar en la sucursal Centro (
sucursal_id = 1)? Resuélvelo conNOT EXISTS. - ¿Qué socios no han hecho ninguna reserva nunca? Resuélvelo de dos maneras: con anti-join y con
NOT EXISTS. - ¿Qué sucursales no tienen ningún ejemplar en estado
prestado? - ¿Qué autores tienen obra en el fondo pero ninguna de sus obras se ha prestado jamás?
Ejercicio 3: Consultas compuestas
- Usando una CTE, obtén los ejemplares disponibles de libros que tienen alguna reserva activa, con su código, título y sucursal. Es la lista de "apartar para reservas".
- Con operadores de conjunto, obtén los identificadores de los socios que han hecho alguna reserva pero nunca un préstamo.
- Lista, para cada sucursal, los socios dados de alta en ella y los ejemplares que custodia… y explica por qué no debe hacerse con un solo
JOINde tres tablas.
Soluciones
Solución 1
-- 1
SELECT su.nombre AS sucursal, e.codigo, l.titulo, e.estado
FROM ejemplares e
INNER JOIN libros l ON l.libro_id = e.libro_id
INNER JOIN sucursales su ON su.sucursal_id = e.sucursal_id
ORDER BY su.nombre, e.codigo;Quince filas. Las dos primeras y las dos últimas:
| sucursal | codigo | titulo | estado |
|---|---|---|---|
| Centro | EJ-3082 | El mapa del tiempo | disponible |
| Centro | EJ-3084 | Los pilares de la Tierra | prestado |
| … | … | … | … |
| Sur | EJ-3089 | Cuadernos de Ravena | disponible |
| Sur | EJ-3094 | Manual de jardinería urbana | baja |
-- 2
SELECT s.nombre || ' ' || s.apellidos AS socio, l.titulo, r.fecha_reserva
FROM reservas r
INNER JOIN socios s ON s.socio_id = r.socio_id
INNER JOIN libros l ON l.libro_id = r.libro_id
WHERE r.estado = 'activa'
ORDER BY r.fecha_reserva;| socio | titulo | fecha_reserva |
|---|---|---|
| Nuria Bastos | El mapa del tiempo | 2026-07-20 |
| Lucía Vendrell | Los pilares de la Tierra | 2026-07-22 |
| Iván Pereda | El mapa del tiempo | 2026-07-28 |
-- 3 LEFT JOIN, porque libros.autor_id admite NULL
SELECT l.titulo, a.apellidos AS autor
FROM libros l
LEFT JOIN autores a ON a.autor_id = l.autor_id
ORDER BY l.titulo;| titulo | autor |
|---|---|
| Álgebra para impacientes | Barreda |
| Cuadernos de Ravena | Sorrentino |
| El invierno de los pájaros | Lemos |
| El mapa del tiempo | Palma |
| La casa de las mareas | Valcárcel |
| Los pilares de la Tierra | Follett |
| Manual de jardinería urbana | Barreda |
| Memoria del Ensanche (1904) | (NULL) |
| Rutas del delta | Ordóñez |
Con INNER JOIN habrías perdido "Memoria del Ensanche (1904)".
-- 4
SELECT s.nombre || ' ' || s.apellidos AS socio,
l.titulo,
p.fecha_devolucion_prevista,
p.fecha_devolucion,
p.recargo
FROM prestamos p
INNER JOIN socios s ON s.socio_id = p.socio_id
INNER JOIN ejemplares e ON e.ejemplar_id = p.ejemplar_id
INNER JOIN libros l ON l.libro_id = e.libro_id
WHERE p.fecha_devolucion > p.fecha_devolucion_prevista
ORDER BY p.fecha_prestamo;| socio | titulo | fecha_devolucion_prevista | fecha_devolucion | recargo |
|---|---|---|---|---|
| Iván Pereda | El mapa del tiempo | 2026-03-26 | 2026-04-02 | 1.40 |
| Lucía Vendrell | Cuadernos de Ravena | 2026-05-03 | 2026-05-10 | 1.40 |
| Sonia Quiroga | Rutas del delta | 2026-06-09 | 2026-06-30 | 4.20 |
Solución 2
-- 1
SELECT l.libro_id, l.titulo
FROM libros l
WHERE NOT EXISTS (SELECT 1 FROM ejemplares e
WHERE e.libro_id = l.libro_id AND e.sucursal_id = 1)
ORDER BY l.libro_id;| libro_id | titulo |
|---|---|
| 334 | Álgebra para impacientes |
| 335 | Cuadernos de Ravena |
| 337 | Rutas del delta |
-- 2a Anti-join
SELECT s.socio_id, s.apellidos
FROM socios s
LEFT JOIN reservas r ON r.socio_id = s.socio_id
WHERE r.reserva_id IS NULL
ORDER BY s.socio_id;
-- 2b NOT EXISTS
SELECT s.socio_id, s.apellidos
FROM socios s
WHERE NOT EXISTS (SELECT 1 FROM reservas r WHERE r.socio_id = s.socio_id)
ORDER BY s.socio_id;| socio_id | apellidos |
|---|---|
| 12 | Quiroga |
| 13 | Etxebarri |
| 17 | Salom |
| 19 | Miralles |
| 20 | Roig |
Han reservado alguna vez los socios 11, 14, 15, 16 y 18; los otros cinco, nunca.
-- 3
SELECT su.sucursal_id, su.nombre
FROM sucursales su
WHERE NOT EXISTS (SELECT 1 FROM ejemplares e
WHERE e.sucursal_id = su.sucursal_id AND e.estado = 'prestado')
ORDER BY su.sucursal_id;| sucursal_id | nombre |
|---|---|
| 3 | Sur |
| 4 | Este |
-- 4 Doble negación: autores CON libros pero SIN préstamos de esos libros
SELECT a.autor_id, a.apellidos
FROM autores a
WHERE EXISTS (SELECT 1 FROM libros l WHERE l.autor_id = a.autor_id)
AND NOT EXISTS (SELECT 1
FROM libros l
INNER JOIN ejemplares e ON e.libro_id = l.libro_id
INNER JOIN prestamos p ON p.ejemplar_id = e.ejemplar_id
WHERE l.autor_id = a.autor_id)
ORDER BY a.autor_id;Ningún autor cumple las dos condiciones. Óscar Barreda (4) parecía candidato, porque "Manual de jardinería urbana" no se ha prestado nunca, pero su otra obra, "Álgebra para impacientes", sí (préstamo 4). Es un buen recordatorio de que en una condición "ninguna de sus obras" hay que examinar todas las obras del autor, no una a una.
Solución 3
-- 1
WITH reservados AS (
SELECT DISTINCT libro_id FROM reservas WHERE estado = 'activa'
)
SELECT e.codigo, l.titulo, su.nombre AS sucursal
FROM ejemplares e
INNER JOIN reservados rv ON rv.libro_id = e.libro_id
INNER JOIN libros l ON l.libro_id = e.libro_id
INNER JOIN sucursales su ON su.sucursal_id = e.sucursal_id
WHERE e.estado = 'disponible'
ORDER BY e.codigo;| codigo | titulo | sucursal |
|---|---|---|
| EJ-3082 | El mapa del tiempo | Centro |
| EJ-3083 | El mapa del tiempo | Sur |
| EJ-3085 | Los pilares de la Tierra | Norte |
Tres ejemplares que el personal debe apartar. El DISTINCT de la CTE es importante: "El mapa del tiempo" tiene dos reservas activas, y sin él cada ejemplar suyo aparecería duplicado.
Los cinco socios que han reservado alguna vez (11, 14, 15, 16, 18) han hecho también algún préstamo. Prueba la operación inversa para ver la diferencia:
| socio_id |
|---|
| 12 |
| 17 |
| 19 |
-- 3 La consulta "ingenua"
SELECT su.nombre, s.apellidos, e.codigo
FROM sucursales su
LEFT JOIN socios s ON s.sucursal_id = su.sucursal_id
LEFT JOIN ejemplares e ON e.sucursal_id = su.sucursal_id;Por qué no debe hacerse así: socios y ejemplares no están relacionadas entre sí; ambas cuelgan de sucursales de forma independiente. Al unirlas en la misma consulta se produce un producto cartesiano dentro de cada sucursal: la sucursal Centro tiene 4 socios y 6 ejemplares, así que genera 24 filas. El total es 4×6 + 3×4 + 2×3 + 1×2 = 24 + 12 + 6 + 2 = 44 filas, ninguna de las cuales significa nada: emparejan a Nuria Bastos con un ejemplar que no ha tocado.
Este fenómeno tiene nombre —explosión de filas o fan trap— y es una de las causas más frecuentes de recuentos inflados. Las dos soluciones correctas:
-- a) Dos consultas separadas, que es lo que pedía la pregunta de verdad
SELECT su.nombre, s.apellidos FROM sucursales su
LEFT JOIN socios s ON s.sucursal_id = su.sucursal_id ORDER BY su.nombre;
SELECT su.nombre, e.codigo FROM sucursales su
LEFT JOIN ejemplares e ON e.sucursal_id = su.sucursal_id ORDER BY su.nombre;-- b) Una sola consulta, resumiendo cada rama por separado antes de unirlas.
-- Esto necesita funciones de agregado: es exactamente el tema
-- de la lección siguiente, 02-05.Conclusión
Esta lección ha convertido las siete tablas aisladas de BiblioRed en un sistema consultable:
- Los datos están repartidos a propósito —cada hecho en un solo sitio— y se recomponen al consultar con la reunión ⋈ del álgebra relacional.
- El producto cartesiano (
CROSS JOIN) es el punto de partida conceptual y el accidente clásico de la sintaxis antigua de comas. INNER JOIN ... ONdevuelve solo lo que empareja; los alias de tabla son casi obligatorios yNATURAL JOINes magia que conviene evitar.LEFT JOINconserva la tabla izquierda, y su combinación conWHERE clave_derecha IS NULL—el anti-join— es el patrón para responder a toda pregunta que empiece por "los que nunca…": socios sin préstamos, ejemplares nunca prestados.RIGHT JOINyFULL OUTER JOINcompletan el cuadro (y llegaron tarde a SQLite, en la versión 3.39).- El
SELF JOINcompara filas de una tabla consigo misma, con la condición>para no duplicar parejas simétricas. - Encadenar cinco tablas —socio → préstamo → ejemplar → libro → autor— responde a las preguntas reales del mostrador, siempre que uses
LEFT JOINdonde la clave ajena admita nulos. - Filtrar en
ONno es lo mismo que filtrar enWHEREen unLEFT JOIN: elWHERElo degrada aINNER JOIN. - Las subconsultas en sus cinco formas: escalar, con
IN, conEXISTS/NOT EXISTS, correlacionadas y como tabla derivada en elFROM. Con una regla grabada a fuego:NOT EXISTSen vez deNOT INcuando pueda haberNULL. - Las CTE con
WITH, que convierten una consulta laberíntica en un procedimiento legible de arriba abajo. - Los operadores de conjunto:
UNION(deduplica),UNION ALL(más rápido),INTERSECTyEXCEPT(que no es simétrico).
Fíjate en que hemos rozado repetidamente un límite: podemos listar los préstamos de cada socio, pero no contarlos; podemos ver los ejemplares de cada sucursal, pero no cuántos hay; el ejercicio 3.3 se ha quedado a medias porque para resumir dos ramas hacía falta algo que aún no tenemos.
Ese algo llega en la lección 02-05, Agregación y Agrupación de Datos: COUNT, SUM, AVG, MIN y MAX, la cláusula GROUP BY, la diferencia entre HAVING y WHERE, el orden lógico en que se ejecuta realmente una consulta y —muy importante después de lo que acabamos de ver— el problema de contar filas infladas por un JOIN. Con ella, BiblioRed pasará de responder "qué hay" a responder "cuánto hay".
Fundamentos de Bases de Datos
Módulo 1: Introducción a las Bases de Datos
- Conceptos Básicos de Bases de Datos
- Tipos de Bases de Datos
- Historia y Evolución de las Bases de Datos
- Sistemas Gestores de Bases de Datos y Arquitectura
Módulo 2: Bases de Datos Relacionales
- Modelo Relacional
- Lenguaje SQL
- Operaciones Básicas en SQL
- Consultas Multitabla: JOIN y Subconsultas
- Agregación y Agrupación de Datos
- Integridad Referencial
Módulo 3: Bases de Datos No Relacionales
- Introducción a NoSQL
- Tipos de Bases de Datos NoSQL
- Modelado de Datos en NoSQL
- Comparación entre Bases de Datos Relacionales y No Relacionales
Módulo 4: Diseño de Esquemas
- Principios de Diseño de Esquemas
- Diagramas Entidad-Relación (ER)
- Transformación de Diagramas ER a Esquemas Relacionales
- Tipos de Datos y Restricciones
Módulo 5: Normalización
Módulo 6: Transacciones, Rendimiento y Seguridad
- Transacciones y Propiedades ACID
- Concurrencia y Niveles de Aislamiento
- Índices y Optimización de Consultas
- Seguridad, Permisos y Copias de Seguridad
Módulo 7: Ejercicios Prácticos
- Ejercicios de SQL
- Ejercicios de Diseño de Esquemas
- Ejercicios de Normalización
- Ejercicios de Consultas Avanzadas y Transacciones
Módulo 8: Casos de Estudio
- Caso de Estudio: Base de Datos Relacional
- Caso de Estudio: Base de Datos No Relacional
- Caso de Estudio: Persistencia Políglota
