Las siete tablas de BiblioRed existen y están vacías. Esta lección las llena y enseña a interrogarlas. Es la lección más práctica del módulo: aquí aprendes las cuatro operaciones que cubren el 90 % del trabajo diario con una base de datos —insertar, consultar, modificar y borrar—, conocidas colectivamente como CRUD (Create, Read, Update, Delete).
Nos limitamos deliberadamente a una sola tabla por consulta. Recomponer información repartida entre varias tablas es un tema con suficiente sustancia para su propia lección (02-04), y agrupar y resumir, para la siguiente (02-05). Aquí construimos los cimientos: sin dominar WHERE y ORDER BY sobre una tabla, ningún JOIN saldrá bien.
El entregable de la lección es el juego de datos de prueba de BiblioRed: cuatro sucursales, ocho autores, diez socios, nueve libros, quince ejemplares, doce préstamos y cinco reservas. Todos ficticios y todos coherentes entre sí. Los usaremos hasta el final del curso, así que ejecútalos con atención y guárdalos en un fichero.
Contenido
INSERT: dar de alta filas- Entregable: el juego de datos de BiblioRed
- Reajustar los generadores de identificadores
SELECT: proyección, alias yDISTINCTWHERE: filtrar filasORDER BY: ordenar el resultadoLIMITyOFFSET: paginaciónUPDATE: modificar filas existentesDELETE: eliminar filasTRUNCATE: vaciar una tabla entera- Errores comunes y consejos
- Ejercicios
- Conclusión
INSERT: dar de alta filas
INSERT: dar de alta filasForma básica
Empecemos por las sucursales de BiblioRed. La red opera en la ciudad ficticia de Vallmar y tiene cuatro bibliotecas.
INSERT INTO sucursales (nombre, direccion, telefono, fecha_apertura)
VALUES ('Centro', 'Plaza Mayor 1', '900 100 001', '1998-04-12');La salida de PostgreSQL se lee así: INSERT <oid> <número de filas insertadas>. El primer número es un vestigio histórico y siempre vale 0; el segundo es el que importa.
Fíjate en tres cosas:
- No hemos dado
sucursal_id. La columna esGENERATED BY DEFAULT AS IDENTITY, así que PostgreSQL le asigna el 1. - La fecha va entre comillas simples, en formato ISO
AAAA-MM-DD. El gestor la convierte aDATEporque la columna es de ese tipo. - El teléfono va entre comillas, aunque parezca un número: es
VARCHAR, como decidimos en la lección anterior.
Sin lista de columnas: la forma frágil
SQL permite omitir la lista de columnas si das un valor para todas, en el orden exacto de la definición de la tabla:
-- Legal, pero no lo hagas
INSERT INTO sucursales VALUES (DEFAULT, 'Norte', 'Avenida del Parque 45', '900 100 002', '2005-09-30');¿Por qué evitarlo? Porque el día que alguien añada una columna con ALTER TABLE, o reordene la definición, todos los INSERT sin lista de columnas se romperán o, peor, insertarán valores en la columna equivocada sin dar error. Escribe siempre la lista de columnas. Es la primera regla de higiene del SQL profesional.
Hagámoslo bien:
INSERT INTO sucursales (nombre, direccion, telefono, fecha_apertura)
VALUES ('Norte', 'Avenida del Parque 45', '900 100 002', '2005-09-30');Esta es la sucursal Norte, que a partir de ahora tendrá sucursal_id = 2.
Insertar varias filas de una vez
Se pueden encadenar varias tuplas de valores separadas por comas. Es más rápido (una sola operación en lugar de N) y más legible:
INSERT INTO sucursales (nombre, direccion, telefono, fecha_apertura) VALUES
('Sur', 'Calle Olivar 12', '900 100 003', '2011-02-18'),
('Este', 'Ronda del Puerto 8', NULL, '2019-06-25');La sucursal Este todavía no tiene teléfono propio: escribimos NULL sin comillas. Si escribiéramos 'NULL' estaríamos guardando la cadena de cuatro letras N-U-L-L, que no es lo mismo en absoluto.
Otra opción es omitir la columna en la lista: lo que no se menciona queda a NULL (o a su valor por defecto, si lo tuviera).
-- (Solo ilustrativo, NO lo ejecutes: crearía una segunda sucursal Este.)
-- Equivalente para el teléfono, pero explícito es mejor que implícito.
INSERT INTO sucursales (nombre, direccion, fecha_apertura)
VALUES ('Este', 'Ronda del Puerto 8', '2019-06-25');RETURNING: saber qué identificador se ha generado
Cuando dejas que el gestor genere la clave, surge un problema práctico inmediato: ¿qué número le ha tocado? Lo necesitarás para insertar las filas hijas. PostgreSQL lo resuelve con RETURNING:
INSERT INTO autores (nombre, apellidos, nacionalidad, anio_nacimiento)
VALUES ('Félix J.', 'Palma', 'española', 1968)
RETURNING autor_id, apellidos;RETURNING funciona igual con UPDATE y DELETE, y puede devolver cualquier expresión, incluido *. Es una extensión de PostgreSQL muy cómoda que evita el clásico "inserto y luego consulto".
SQLite no tiene RETURNING hasta la versión 3.35 (2021); en versiones anteriores se usa la función last_insert_rowid():
Qué comprueba el gestor en cada INSERT
Antes de aceptar la fila, el gestor verifica las tres reglas de integridad de la lección 02-01:
ERROR: llave duplicada viola restricción de unicidad «uq_sucursales_nombre»
DETALLE: Ya existe la llave (nombre)=(Norte).INSERT INTO socios (nombre, apellidos, fecha_alta, sucursal_id, activo)
VALUES ('Prueba', 'Prueba', '2026-01-01', 99, TRUE);ERROR: inserción o actualización en la tabla «socios» viola la llave foránea «fk_socios_sucursal»
DETALLE: La llave (sucursal_id)=(99) no está presente en la tabla «sucursales».Esto es exactamente lo que la hoja de cálculo de BiblioRed no hacía. Aquí el error no es una molestia: es el sistema haciendo su trabajo.
- Entregable: el juego de datos de BiblioRed
Este es el script de carga. Ejecútalo entero y en este orden (las hijas necesitan a las padres) y guárdalo como datos_biblioredb.sql.
Un matiz sobre los identificadores que verás:
- En
sucursales,autores,ejemplares,prestamosyreservasdejamos que el gestor los genere. - En
sociosylibroslos imponemos: BiblioRed está migrando desde la hoja de cálculo y quiere conservar los números de carné (Marta Alsina es la socia 14 desde 2021) y las signaturas del catálogo antiguo (331 es "El mapa del tiempo"). Es un caso realísimo, y en el apartado 3 veremos el efecto secundario que provoca.
-- ============================================================
-- BiblioRed - Juego de datos de prueba (todos ficticios)
-- Módulo 2, lección 02-03. Dialecto: PostgreSQL
-- ============================================================
-- PASO 0: partir de cero. Los ejemplos del apartado anterior ya
-- insertaron dos sucursales y un autor; los borramos para que el
-- script sea la única fuente de datos. El orden es el inverso al
-- de dependencias: primero las hijas, después las padres.
DELETE FROM reservas;
DELETE FROM prestamos;
DELETE FROM ejemplares;
DELETE FROM libros;
DELETE FROM socios;
DELETE FROM autores;
DELETE FROM sucursales;
ALTER TABLE sucursales ALTER COLUMN sucursal_id RESTART WITH 1;
ALTER TABLE autores ALTER COLUMN autor_id RESTART WITH 1;
ALTER TABLE ejemplares ALTER COLUMN ejemplar_id RESTART WITH 1;
ALTER TABLE prestamos ALTER COLUMN prestamo_id RESTART WITH 1;
ALTER TABLE reservas ALTER COLUMN reserva_id RESTART WITH 1;
-- SUCURSALES: las cuatro bibliotecas de la red de Vallmar.
-- Identificadores 1..4 generados por el gestor: Centro=1, Norte=2, Sur=3, Este=4.
INSERT INTO sucursales (nombre, direccion, telefono, fecha_apertura) VALUES
('Centro', 'Plaza Mayor 1', '900 100 001', '1998-04-12'),
('Norte', 'Avenida del Parque 45', '900 100 002', '2005-09-30'),
('Sur', 'Calle Olivar 12', '900 100 003', '2011-02-18'),
('Este', 'Ronda del Puerto 8', NULL, '2019-06-25');
-- AUTORES: identificadores 1..8 generados por el gestor.
-- Marina Escolá (8) aún no tiene ninguna obra en el fondo:
-- nos servirá para practicar los LEFT JOIN de la próxima lección.
INSERT INTO autores (nombre, apellidos, nacionalidad, anio_nacimiento) VALUES
('Félix J.', 'Palma', 'española', 1968),
('Ken', 'Follett', 'británica', 1949),
('Irene', 'Valcárcel', 'española', 1975),
('Óscar', 'Barreda', 'española', 1981),
('Nadia', 'Sorrentino', 'italiana', 1970),
('Hugo', 'Lemos', 'portuguesa', 1958),
('Clara', 'Ordóñez', 'española', 1988),
('Marina', 'Escolá', 'española', 1992);
-- SOCIOS: números de carné heredados de la hoja de cálculo (11..20).
-- Pau Miralles (19) no facilitó correo: su email queda a NULL.
-- Ramón Etxebarri (13) está de baja: activo = FALSE.
INSERT INTO socios (socio_id, nombre, apellidos, email, fecha_alta, sucursal_id, activo) VALUES
(11, 'Álvaro', 'Ferrán', '[email protected]', '2018-01-22', 1, TRUE),
(12, 'Sonia', 'Quiroga', '[email protected]', '2019-05-03', 3, TRUE),
(13, 'Ramón', 'Etxebarri', '[email protected]', '2020-11-14', 1, FALSE),
(14, 'Marta', 'Alsina', '[email protected]', '2021-03-08', 2, TRUE),
(15, 'Iván', 'Pereda', '[email protected]', '2021-09-19', 2, TRUE),
(16, 'Nuria', 'Bastos', '[email protected]', '2022-01-30', 1, TRUE),
(17, 'Diego', 'Salom', '[email protected]', '2023-02-11', 3, TRUE),
(18, 'Lucía', 'Vendrell', '[email protected]', '2023-07-05', 4, TRUE),
(19, 'Pau', 'Miralles', NULL, '2024-04-16', 2, TRUE),
(20, 'Elena', 'Roig', '[email protected]', '2024-10-01', 1, TRUE);
-- LIBROS: signaturas heredadas (331..339).
-- El 339 es una publicación municipal antigua: SIN ISBN y SIN autor
-- catalogado. Es la prueba viviente de por qué el ISBN no podía ser
-- clave primaria (lección 02-01).
INSERT INTO libros (libro_id, isbn, titulo, autor_id, editorial, anio_publicacion, idioma) VALUES
(331, '9788401339097', 'El mapa del tiempo', 1, 'Editorial Andana', 2008, 'es'),
(332, '9788401337208', 'Los pilares de la Tierra', 2, 'Editorial Andana', 1989, 'es'),
(333, '9788412007701', 'La casa de las mareas', 3, 'Ediciones Marlia', 2015, 'es'),
(334, '9788412007702', 'Álgebra para impacientes', 4, 'Prensa Técnica Norte', 2019, 'es'),
(335, '9788412007703', 'Cuadernos de Ravena', 5, 'Ediciones Marlia', 2012, 'es'),
(336, '9788412007704', 'El invierno de los pájaros', 6, 'Editorial Andana', 2021, 'es'),
(337, '9788412007705', 'Rutas del delta', 7, 'Ediciones Marlia', 2017, 'ca'),
(338, '9788412007706', 'Manual de jardinería urbana', 4, 'Prensa Técnica Norte', 2023, 'es'),
(339, NULL, 'Memoria del Ensanche (1904)', NULL, 'Ayuntamiento de Vallmar', 1904, 'es');
-- EJEMPLARES: los objetos físicos. Identificadores 1..15 generados
-- en el orden de esta lista; EJ-3081 será el ejemplar_id 1.
INSERT INTO ejemplares (codigo, libro_id, sucursal_id, estado, fecha_adquisicion) VALUES
('EJ-3081', 331, 2, 'prestado', '2019-03-14'),
('EJ-3082', 331, 1, 'disponible', '2019-03-14'),
('EJ-3083', 331, 3, 'disponible', '2021-06-01'),
('EJ-3084', 332, 1, 'prestado', '2015-11-20'),
('EJ-3085', 332, 2, 'disponible', '2015-11-20'),
('EJ-3086', 333, 1, 'prestado', '2016-02-09'),
('EJ-3087', 334, 2, 'disponible', '2020-01-15'),
('EJ-3088', 334, 4, 'reparacion', '2020-01-15'),
('EJ-3089', 335, 3, 'disponible', '2013-05-04'),
('EJ-3090', 336, 2, 'prestado', '2022-04-27'),
('EJ-3091', 336, 1, 'disponible', '2022-04-27'),
('EJ-3092', 337, 4, 'disponible', '2018-10-02'),
('EJ-3093', 338, 1, 'disponible', '2023-09-11'),
('EJ-3094', 338, 3, 'baja', '2023-09-11'),
('EJ-3095', 339, 1, 'disponible', '2003-01-15');
-- PRESTAMOS: ocho cerrados y cuatro abiertos.
-- fecha_devolucion NULL = préstamo todavía en curso.
-- Los cuatro préstamos abiertos corresponden a los cuatro ejemplares
-- cuyo estado es 'prestado' (1, 4, 6 y 10). Coherencia total.
INSERT INTO prestamos
(socio_id, ejemplar_id, fecha_prestamo, fecha_devolucion_prevista, fecha_devolucion, recargo) VALUES
(14, 2, '2026-03-02', '2026-03-23', '2026-03-19', 0.00),
(15, 1, '2026-03-05', '2026-03-26', '2026-04-02', 1.40),
(16, 5, '2026-03-11', '2026-04-01', '2026-03-30', 0.00),
(14, 7, '2026-04-06', '2026-04-27', '2026-04-25', 0.00),
(18, 9, '2026-04-12', '2026-05-03', '2026-05-10', 1.40),
(16, 4, '2026-05-04', '2026-05-25', '2026-05-22', 0.00),
(11, 1, '2026-05-08', '2026-05-29', '2026-05-27', 0.00),
(12, 12, '2026-05-19', '2026-06-09', '2026-06-30', 4.20),
(14, 1, '2026-07-14', '2026-08-04', NULL, NULL),
(15, 4, '2026-07-18', '2026-08-08', NULL, NULL),
(17, 6, '2026-07-21', '2026-08-11', NULL, NULL),
(19, 10, '2026-07-25', '2026-08-15', NULL, NULL);
-- RESERVAS: apuntan al LIBRO (la obra), no al ejemplar.
INSERT INTO reservas (socio_id, libro_id, fecha_reserva, fecha_expiracion, estado) VALUES
(16, 331, '2026-07-20', '2026-08-10', 'activa'),
(18, 332, '2026-07-22', '2026-08-12', 'activa'),
(14, 336, '2026-06-30', '2026-07-20', 'atendida'),
(11, 333, '2026-07-05', '2026-07-25', 'cancelada'),
(15, 331, '2026-07-28', '2026-08-18', 'activa');Comprobación de la carga
SELECT 'sucursales' AS tabla, COUNT(*) AS filas FROM sucursales
UNION ALL SELECT 'autores', COUNT(*) FROM autores
UNION ALL SELECT 'socios', COUNT(*) FROM socios
UNION ALL SELECT 'libros', COUNT(*) FROM libros
UNION ALL SELECT 'ejemplares', COUNT(*) FROM ejemplares
UNION ALL SELECT 'prestamos', COUNT(*) FROM prestamos
UNION ALL SELECT 'reservas', COUNT(*) FROM reservas;(COUNT y UNION ALL son de las lecciones 02-05 y 02-04; aquí solo lo usamos como recuento de control.)
| tabla | filas |
|---|---|
| sucursales | 4 |
| autores | 8 |
| socios | 10 |
| libros | 9 |
| ejemplares | 15 |
| prestamos | 12 |
| reservas | 5 |
Y la correspondencia entre códigos e identificadores de ejemplar, que necesitarás para entender los préstamos:
| ejemplar_id | codigo | libro_id |
|---|---|---|
| 1 | EJ-3081 | 331 |
| 2 | EJ-3082 | 331 |
| 3 | EJ-3083 | 331 |
| 4 | EJ-3084 | 332 |
Si tus recuentos coinciden, tienes el mismo juego de datos que el resto del curso.
Notas para SQLite
Tres cambios en el script:
TRUE/FALSEensocios.activo→1/0.- Las líneas
ALTER TABLE ... RESTART WITHdel paso 0 no existen en SQLite: elimínalas. ConINTEGER PRIMARY KEYsinAUTOINCREMENT, el siguiente identificador se calcula solo a partir del máximo existente. - Las fechas se escriben igual (
'2026-03-02'), pero se guardan como texto. Mientras uses el formato ISO, las comparaciones y ordenaciones seguirán funcionando, porque el orden alfabético deAAAA-MM-DDcoincide con el cronológico. Es exactamente la razón por la que ese formato es el bueno.
- Reajustar los generadores de identificadores
Aquí llega el efecto secundario prometido. En socios y libros insertamos identificadores explícitos, pero el generador de la columna no se ha enterado: sigue apuntando al 1. Comprobémoslo dando de alta un socio sin indicar su identificador:
INSERT INTO socios (nombre, apellidos, fecha_alta, sucursal_id, activo)
VALUES ('Socio', 'Deprueba', '2026-08-01', 1, TRUE)
RETURNING socio_id;El nuevo socio ha recibido el 1, cuando los carnés de BiblioRed empiezan en el 11. Nada ha fallado, pero el desajuste ya está sembrado: los diez siguientes de alta se llevarían el 2, el 3… y el undécimo chocaría:
ERROR: llave duplicada viola restricción de unicidad «pk_socios»
DETALLE: Ya existe la llave (socio_id)=(11).Es uno de los errores más desconcertantes para quien empieza, porque aparece semanas después de la carga y sin relación aparente con ella. La causa siempre es la misma: se han insertado claves a mano sin resincronizar el generador. Borremos el socio de prueba y arreglémoslo:
La solución en PostgreSQL:
ALTER TABLE socios ALTER COLUMN socio_id RESTART WITH 21;
ALTER TABLE libros ALTER COLUMN libro_id RESTART WITH 340;O, de forma automática y sin tener que mirar el máximo a ojo:
En SQLite el problema no existe si la clave se declaró como INTEGER PRIMARY KEY sin AUTOINCREMENT: el siguiente valor se calcula como el máximo actual más uno, así que se ajusta solo.
SELECT: proyección, alias y DISTINCT
SELECT: proyección, alias y DISTINCTSELECT es la instrucción más usada de SQL y la que más lecciones ocupa en este curso. Su forma mínima:
Proyección: elegir columnas
| nombre | apellidos | fecha_alta |
|---|---|---|
| Álvaro | Ferrán | 2018-01-22 |
| Sonia | Quiroga | 2019-05-03 |
| Ramón | Etxebarri | 2020-11-14 |
| Marta | Alsina | 2021-03-08 |
| Iván | Pereda | 2021-09-19 |
| Nuria | Bastos | 2022-01-30 |
| Diego | Salom | 2023-02-11 |
| Lucía | Vendrell | 2023-07-05 |
| Pau | Miralles | 2024-04-16 |
| Elena | Roig | 2024-10-01 |
Esto es la proyección π del álgebra relacional. Recuerda que el orden en que aparecen las filas no está garantizado sin ORDER BY: aquí salen en orden de inserción porque la tabla es pequeña y recién cargada, pero no cuentes con ello.
SELECT *: cómodo y peligroso
| sucursal_id | nombre | direccion | telefono | fecha_apertura |
|---|---|---|---|---|
| 1 | Centro | Plaza Mayor 1 | 900 100 001 | 1998-04-12 |
| 2 | Norte | Avenida del Parque 45 | 900 100 002 | 2005-09-30 |
| 3 | Sur | Calle Olivar 12 | 900 100 003 | 2011-02-18 |
| 4 | Este | Ronda del Puerto 8 | (NULL) | 2019-06-25 |
* significa "todas las columnas". Está perfecto para explorar en la consola, pero no lo uses en código de aplicación: traes datos que no necesitas por la red, y si mañana alguien añade una columna, tu programa recibe algo que no esperaba. En un script guardado, columnas explícitas.
Observa que el NULL del teléfono de Este aparece como celda vacía en psql. Puedes hacerlo visible:
Alias con AS
Un alias renombra una columna en el resultado, sin tocar la tabla. Es el operador de renombrado ρ del álgebra relacional.
SELECT codigo AS etiqueta,
estado AS situacion,
fecha_adquisicion AS "fecha de compra"
FROM ejemplares
WHERE sucursal_id = 4;| etiqueta | situacion | fecha de compra |
|---|---|---|
| EJ-3088 | reparacion | 2020-01-15 |
| EJ-3092 | disponible | 2018-10-02 |
Puntos a retener:
ASes opcional (codigo etiquetafunciona igual), pero escribirlo hace el SQL mucho más legible.- Para que un alias lleve espacios, mayúsculas o tildes hay que ponerlo entre comillas dobles: es un identificador. Aquí sí es aceptable, porque el alias solo vive en la salida.
También se pueden calcular columnas nuevas:
| nombre_completo | fecha_alta |
|---|---|
| Marta Alsina | 2021-03-08 |
| Iván Pereda | 2021-09-19 |
| Pau Miralles | 2024-04-16 |
|| es el operador estándar de concatenación (funciona en PostgreSQL y SQLite; MySQL usa CONCAT()).
Cuidado con NULL en las concatenaciones: 'Pau' || NULL da NULL, no 'Pau'. Si apellidos pudiera ser nulo, el nombre completo desaparecería entero. En 02-05 veremos COALESCE, que resuelve exactamente esto.
DISTINCT: eliminar duplicados
Devuelve 15 filas con muchas repeticiones. Con DISTINCT:
| estado |
|---|
| baja |
| disponible |
| prestado |
| reparacion |
DISTINCT se aplica a la combinación completa de columnas seleccionadas, no a la primera:
| sucursal_id | estado |
|---|---|
| 1 | disponible |
| 1 | prestado |
| 2 | disponible |
| 2 | prestado |
| 3 | baja |
| 3 | disponible |
| 4 | disponible |
| 4 | reparacion |
Son las ocho combinaciones distintas que existen, de las 15 filas originales.
Recuerda de la lección 02-01: en el álgebra relacional la proyección siempre elimina duplicados; en SQL hay que pedirlo. DISTINCT obliga al gestor a ordenar o construir una tabla de dispersión, así que tiene coste: no lo pongas "por si acaso".
WHERE: filtrar filas
WHERE: filtrar filasWHERE es la selección σ del álgebra: se queda con las filas cuya condición sea VERDADERO (recuerda: DESCONOCIDO no pasa).
Operadores de comparación
| Operador | Significado |
|---|---|
= |
Igual |
<> o != |
Distinto |
<, >, <=, >= |
Menor, mayor, menor o igual, mayor o igual |
SELECT titulo, anio_publicacion
FROM libros
WHERE anio_publicacion > 2015
ORDER BY anio_publicacion;| titulo | anio_publicacion |
|---|---|
| Rutas del delta | 2017 |
| Álgebra para impacientes | 2019 |
| El invierno de los pájaros | 2021 |
| Manual de jardinería urbana | 2023 |
Las comparaciones funcionan también sobre texto (orden alfabético según la configuración regional) y sobre fechas:
SELECT nombre, apellidos, fecha_alta
FROM socios
WHERE fecha_alta >= '2023-01-01'
ORDER BY fecha_alta;| nombre | apellidos | fecha_alta |
|---|---|---|
| Diego | Salom | 2023-02-11 |
| Lucía | Vendrell | 2023-07-05 |
| Pau | Miralles | 2024-04-16 |
| Elena | Roig | 2024-10-01 |
Operadores lógicos: AND, OR, NOT
| codigo | estado | sucursal_id |
|---|---|---|
| EJ-3082 | disponible | 1 |
| EJ-3091 | disponible | 1 |
| EJ-3093 | disponible | 1 |
| EJ-3095 | disponible | 1 |
AND tiene más precedencia que OR, igual que la multiplicación sobre la suma. Esto provoca errores silenciosos:
-- Lo que se quería: los ejemplares de las sucursales 1 o 2 que estén prestados
-- Lo que hace: los de la sucursal 1 (en cualquier estado)
-- MÁS los de la 2 que estén prestados
SELECT codigo, sucursal_id, estado FROM ejemplares
WHERE sucursal_id = 1 OR sucursal_id = 2 AND estado = 'prestado';Devuelve 7 filas: los seis de la sucursal 1 más EJ-3081. Con paréntesis:
SELECT codigo, sucursal_id, estado FROM ejemplares
WHERE (sucursal_id = 1 OR sucursal_id = 2) AND estado = 'prestado';| codigo | sucursal_id | estado |
|---|---|---|
| EJ-3081 | 2 | prestado |
| EJ-3084 | 1 | prestado |
| EJ-3086 | 1 | prestado |
Consejo: usa paréntesis siempre que mezcles AND y OR, aunque sepas la precedencia. Quien lea tu consulta dentro de un año lo agradecerá.
BETWEEN: rangos
SELECT titulo, anio_publicacion
FROM libros
WHERE anio_publicacion BETWEEN 2010 AND 2019
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 |
BETWEEN a AND b es azúcar sintáctico de >= a AND <= b: ambos extremos están incluidos. Es la fuente de un error clásico con fechas y horas: BETWEEN '2026-03-01' AND '2026-03-31' sobre una columna TIMESTAMP deja fuera todo lo ocurrido el día 31 después de medianoche, porque 2026-03-31 09:00 es mayor que 2026-03-31 00:00. Con columnas DATE como las de BiblioRed no hay problema.
IN: pertenencia a una lista
| codigo | estado |
|---|---|
| EJ-3088 | reparacion |
| EJ-3094 | baja |
IN equivale a una cadena de OR, pero es mucho más legible. Existe también NOT IN:
Mismo resultado que antes.
Aviso importante sobre
NOT INyNULL: si la lista contiene unNULL,NOT INno devuelve ninguna fila, por la lógica de tres valores de la lección 02-01.x NOT IN (1, 2, NULL)equivale ax <> 1 AND x <> 2 AND x <> NULL, y ese último término es siempre DESCONOCIDO. Con listas escritas a mano no ocurre; con listas que vienen de una subconsulta (lección 02-04) es una trampa habitual.
LIKE e ILIKE: búsqueda de patrones en texto
Dos comodines:
| Comodín | Significa |
|---|---|
% |
Cero o más caracteres cualesquiera |
_ |
Exactamente un carácter cualquiera |
| titulo |
|---|
| El mapa del tiempo |
| Rutas del delta |
| Memoria del Ensanche (1904) |
LIKE distingue mayúsculas y minúsculas en PostgreSQL:
Ningún título empieza por el en minúscula. Para ignorar la caja, PostgreSQL ofrece ILIKE (la I es de insensitive), que no es estándar pero es comodísimo:
| titulo |
|---|
| El mapa del tiempo |
| El invierno de los pájaros |
Alternativa portable, válida también en SQLite:
En SQLite, LIKE ya es insensible a mayúsculas para caracteres ASCII (pero no para vocales acentuadas), y no existe ILIKE. Es una de las diferencias que más sorprenden al portar consultas.
Ejemplo con _:
| codigo |
|---|
| EJ-3080…EJ-3089 → los diez de la primera decena |
Concretamente devuelve EJ-3081 a EJ-3089: nueve filas, porque EJ-3080 no existe.
IS NULL: la ausencia de valor
Aquí volvemos a la trampa que anunciamos en 02-01.
| socio_id | nombre | apellidos |
|---|---|---|
| 19 | Pau | Miralles |
Y su complementario, que en BiblioRed tiene un significado muy concreto:
-- Los préstamos todavía abiertos: no hay fecha de devolución
SELECT prestamo_id, socio_id, ejemplar_id, fecha_devolucion_prevista
FROM prestamos
WHERE fecha_devolucion IS NULL
ORDER BY fecha_devolucion_prevista;| prestamo_id | socio_id | ejemplar_id | fecha_devolucion_prevista |
|---|---|---|---|
| 9 | 14 | 1 | 2026-08-04 |
| 10 | 15 | 4 | 2026-08-08 |
| 11 | 17 | 6 | 2026-08-11 |
| 12 | 19 | 10 | 2026-08-15 |
Cuatro préstamos abiertos, que coinciden exactamente con los cuatro ejemplares en estado prestado. La base de datos es coherente.
Y una comprobación de la otra trampa de los NULL, la de las desigualdades:
SELECT COUNT(*) FROM prestamos WHERE recargo <> 0; -- 3
SELECT COUNT(*) FROM prestamos WHERE recargo = 0; -- 5
-- 3 + 5 = 8, no 12: faltan los cuatro préstamos con recargo NULLNinguna de las dos consultas ve los NULL. Si quieres los préstamos "sin recargo pendiente", tienes que decirlo: WHERE recargo = 0 OR recargo IS NULL.
ORDER BY: ordenar el resultado
ORDER BY: ordenar el resultadoComo una relación es un conjunto y no tiene orden, ORDER BY es la única forma de garantizarlo.
| titulo | anio_publicacion |
|---|---|
| Manual de jardinería urbana | 2023 |
| El invierno de los pájaros | 2021 |
| Álgebra para impacientes | 2019 |
| Rutas del delta | 2017 |
| La casa de las mareas | 2015 |
| Cuadernos de Ravena | 2012 |
| El mapa del tiempo | 2008 |
| Los pilares de la Tierra | 1989 |
| Memoria del Ensanche (1904) | 1904 |
ASC (ascendente) es el valor por defecto; DESC invierte.
Varios criterios
Se ordena por el primero y, dentro de los empates, por el segundo:
| sucursal_id | apellidos | nombre |
|---|---|---|
| 1 | Bastos | Nuria |
| 1 | Etxebarri | Ramón |
| 1 | Ferrán | Álvaro |
| 1 | Roig | Elena |
| 2 | Alsina | Marta |
| 2 | Miralles | Pau |
| 2 | Pereda | Iván |
| 3 | Quiroga | Sonia |
| 3 | Salom | Diego |
| 4 | Vendrell | Lucía |
Cada criterio lleva su propio ASC/DESC: ORDER BY sucursal_id ASC, fecha_alta DESC es perfectamente válido.
NULLS FIRST y NULLS LAST
¿Dónde va un NULL al ordenar? El estándar deja libertad, y PostgreSQL los coloca al final en ASC y al principio en DESC (equivale a tratarlos como el valor más grande). SQLite hace lo contrario: los pone al principio en ASC.
Como no queremos depender del gestor, se especifica:
SELECT prestamo_id, fecha_devolucion
FROM prestamos
ORDER BY fecha_devolucion DESC NULLS LAST, prestamo_id;| prestamo_id | fecha_devolucion |
|---|---|
| 8 | 2026-06-30 |
| 7 | 2026-05-27 |
| 6 | 2026-05-22 |
| 5 | 2026-05-10 |
| 4 | 2026-04-25 |
| 2 | 2026-04-02 |
| 3 | 2026-03-30 |
| 1 | 2026-03-19 |
| 9 | (NULL) |
| 10 | (NULL) |
| 11 | (NULL) |
| 12 | (NULL) |
Fíjate en el segundo criterio, prestamo_id: sin él, el orden entre los cuatro NULL sería arbitrario. Cuando el orden importe de verdad, termina siempre por un criterio que desempate sin ambigüedad, típicamente la clave primaria.
NULLS FIRST/NULLS LAST es sintaxis estándar y funciona en PostgreSQL; SQLite la admite desde la versión 3.30.
Ordenar por alias o por posición
SELECT nombre || ' ' || apellidos AS nombre_completo FROM socios ORDER BY nombre_completo;
SELECT nombre, apellidos FROM socios ORDER BY 2; -- por la 2.ª columna: apellidosOrdenar por alias es legítimo y legible. Ordenar por número de posición funciona, pero es frágil: si alguien reordena la lista del SELECT, la consulta cambia de sentido en silencio. Evítalo.
Ordenación de texto y acentos
ORDER BY titulo sitúa "Álgebra para impacientes" en primer lugar si la base de datos usa una configuración regional española, porque Á se ordena junto a A. Con la configuración C (byte a byte), Á iría después de la Z. No es un fallo: es la colación. Puedes ver la tuya con SHOW lc_collate; en PostgreSQL.
LIMIT y OFFSET: paginación
LIMIT y OFFSET: paginación| titulo |
|---|
| Álgebra para impacientes |
| Cuadernos de Ravena |
| El invierno de los pájaros |
OFFSET salta filas antes de empezar a contar:
| titulo |
|---|
| El mapa del tiempo |
| La casa de las mareas |
| Los pilares de la Tierra |
Esta es la mecánica de la paginación de cualquier catálogo web: la página N se obtiene con LIMIT tamaño OFFSET (N-1) * tamaño.
Dos advertencias:
LIMITsinORDER BYno tiene sentido. "Dame 3 filas de las 9" sin decir cuáles significa "dame 3 cualesquiera", y pueden ser distintas en cada ejecución. Peor todavía: si paginas sin ordenar, la misma fila puede salir en la página 1 y en la 3, y otra no salir nunca.OFFSETgrande es lento. Para llegar a la fila 100.000 el gestor tiene que producir y descartar las 100.000 anteriores. En catálogos grandes se usan técnicas de paginación por clave; es tema de rendimiento (lección 06-03).
La sintaxis estándar es OFFSET 3 ROWS FETCH FIRST 3 ROWS ONLY, más verbosa y menos usada. LIMIT/OFFSET funciona en PostgreSQL, SQLite y MySQL.
UPDATE: modificar filas existentes
UPDATE: modificar filas existentesAntes de empezar: los ejemplos de este apartado y del siguiente modifican el juego de datos que acabamos de cargar, y las lecciones 02-04 y 02-05 lo dan por bueno. Al final de cada ejemplo incluimos la instrucción que deshace el cambio. Ejecútala.
El hábito que salva bases de datos
Antes de cualquier UPDATE o DELETE, ejecuta la misma condición con un SELECT. Si el SELECT devuelve las filas que esperabas, la modificación también las tocará a ellas.
-- Paso 1: comprobar el alcance
SELECT socio_id, nombre, apellidos, email FROM socios WHERE socio_id = 19;| socio_id | nombre | apellidos | |
|---|---|---|---|
| 19 | Pau | Miralles | (NULL) |
-- Paso 2: ahora sí, modificar
UPDATE socios SET email = '[email protected]' WHERE socio_id = 19;UPDATE 1 confirma que se ha tocado exactamente una fila. Si ves un número mayor del esperado, algo ha ido mal, y en PostgreSQL, si estás dentro de una transacción, todavía estás a tiempo (lección 06-01).
-- Paso 3: deshacer, para que el juego de datos siga como estaba
UPDATE socios SET email = NULL WHERE socio_id = 19;Actualizar varias columnas y usar el valor anterior
Un UPDATE puede calcular el valor nuevo a partir del actual:
SELECT prestamo_id, recargo FROM prestamos WHERE prestamo_id = 8; -- 4.20
UPDATE prestamos
SET recargo = recargo + 0.50
WHERE prestamo_id = 8;| prestamo_id | recargo |
|---|---|
| 8 | 4.70 |
Importante: recargo + 0.50 sobre un NULL da NULL. Si hubiéramos ejecutado ese UPDATE sin WHERE, los cuatro préstamos abiertos habrían pasado de NULL a… NULL, y los otros ocho habrían subido de precio. Silenciosamente.
Un caso realista con dos instrucciones
Cuando Marta Alsina devuelve EJ-3081, hay que tocar dos tablas:
-- 1) Cerrar el préstamo
UPDATE prestamos
SET fecha_devolucion = '2026-08-01', recargo = 0.00
WHERE prestamo_id = 9;
-- 2) Liberar el ejemplar
UPDATE ejemplares
SET estado = 'disponible'
WHERE codigo = 'EJ-3081';Esto plantea una pregunta incómoda: ¿qué pasa si la primera instrucción funciona y la segunda falla? La base quedaría en un estado inconsistente: un préstamo cerrado y un ejemplar marcado como prestado. La respuesta es la transacción, y es el contenido de la lección 06-01. De momento, deshacemos:
UPDATE prestamos SET fecha_devolucion = NULL, recargo = NULL WHERE prestamo_id = 9;
UPDATE ejemplares SET estado = 'prestado' WHERE codigo = 'EJ-3081';El WHERE olvidado
-- CATASTRÓFICO: pone el mismo correo a los diez socios
UPDATE socios SET email = '[email protected]';Y además violaría uq_socios_email, así que en este caso concreto la restricción nos salvaría. No siempre habrá una restricción que te salve. Un UPDATE ejemplares SET estado = 'baja'; habría dado de baja los 40.000 ejemplares de BiblioRed sin protestar.
Costumbres que evitan el desastre:
- Escribir el
WHEREantes que elSET. Empieza tecleandoUPDATE tabla WHERE ...y luego vuelve a insertar elSET. Suena raro, funciona. - Probar con
SELECTprimero. Siempre. - Trabajar dentro de una transacción en las operaciones delicadas (06-01).
- En
psql, activar\set ON_ERROR_STOP onen los scripts para que se detengan al primer error.
DELETE: eliminar filas
DELETE: eliminar filasVamos a crear una fila de usar y tirar para no estropear el juego de datos:
-- Alta de prueba
INSERT INTO socios (socio_id, nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES (99, 'Bruno', 'Temporal', '[email protected]', '2026-08-01', 1, TRUE);| socio_id | nombre | apellidos |
|---|---|---|
| 99 | Bruno | Temporal |
En PostgreSQL puedes usar RETURNING también aquí, para dejar constancia de lo que se llevó por delante:
DELETE y la integridad referencial
Intenta borrar un socio que tiene préstamos:
ERROR: update or delete on table "socios" violates foreign key constraint
"fk_prestamos_socio" on table "prestamos"
DETALLE: La llave (socio_id)=(14) todavía es referida desde la tabla «prestamos».El gestor te está protegiendo. Si permitiera el borrado, los tres préstamos de Marta Alsina quedarían huérfanos: apuntando a un socio que ya no existe. Ese comportamiento —y sus alternativas, como el borrado en cascada— es el tema íntegro de la lección 02-06.
En SQLite, si olvidaste PRAGMA foreign_keys = ON, ese DELETE funcionará y te dejará tres filas huérfanas sin decir nada. Es exactamente el escenario que la lección 02-06 te enseñará a detectar y limpiar.
El WHERE olvidado, versión definitiva
Sin WHERE, DELETE vacía la tabla entera, sin preguntar y sin papelera. Si te ocurre fuera de una transacción, la única salida es la copia de seguridad (lección 06-04). Las cuatro costumbres del apartado anterior valen aquí con más razón todavía.
TRUNCATE: vaciar una tabla entera
TRUNCATE: vaciar una tabla enteraCuando lo que quieres de verdad es vaciar una tabla completa, existe una instrucción específica:
DELETE FROM tabla |
TRUNCATE TABLE tabla |
|
|---|---|---|
| Sublenguaje | DML | DDL |
Admite WHERE |
Sí | No |
| Velocidad en tablas grandes | Lenta: borra fila a fila | Casi instantánea |
| Registra cada fila borrada | Sí | No |
| Reinicia el generador de identificadores | No | Opcionalmente (RESTART IDENTITY) |
Se puede deshacer con ROLLBACK |
Sí | En PostgreSQL sí; en otros gestores no |
-- Vaciar y reiniciar el contador, arrastrando las tablas dependientes
TRUNCATE TABLE prestamos, reservas RESTART IDENTITY;SQLite no tiene TRUNCATE; usa DELETE FROM tabla sin WHERE, que internamente optimiza.
No ejecutes ningún
TRUNCATEsobre tubiblioredb: perderías el juego de datos que acabas de cargar. Si ya lo has hecho, vuelve a ejecutar el script del apartado 2.
Errores Comunes y Consejos
UPDATEoDELETEsinWHERE. El error más caro que se comete con SQL. Prueba siempre la condición con unSELECTantes.- Escribir
'NULL'en lugar deNULL. Con comillas es una cadena de texto de cuatro letras; sin comillas, la ausencia de valor. Una columna que mezcla ambas cosas es una columna arruinada. - Usar
= NULLo<> NULL. Cero filas, sin error. SiempreIS NULL/IS NOT NULL. - Olvidar que
<> valorexcluye losNULL.WHERE recargo <> 0no devuelve los préstamos con recargo nulo. Si los quieres, añadeOR recargo IS NULL. - Mezclar
ANDyORsin paréntesis.ANDgana. Pon paréntesis siempre. INSERTsin lista de columnas. Se rompe en cuanto alguien toca el esquema, a veces en silencio.- Insertar identificadores explícitos y no resincronizar la secuencia. Provoca errores de clave duplicada mucho después, cuando ya nadie recuerda la carga inicial.
LIMITsinORDER BY. El resultado no es reproducible y la paginación puede repetir u omitir filas.- Fiarse de que
LIKEdistingue mayúsculas. En PostgreSQL sí, en SQLite (ASCII) no, en MySQL depende de la colación. Si necesitas certeza,LOWER(columna) LIKE .... - Consejo: en
psql,\pset null '(nulo)'hace visibles losNULLy\x onmuestra los resultados en vertical, ideal para filas anchas. - Consejo: guarda todo el SQL en ficheros (
esquema_biblioredb.sql,datos_biblioredb.sql) y ejecútalos con\i fichero.sqlenpsqlo.read fichero.sqlensqlite3. Poder reconstruir la base en diez segundos te dará libertad para experimentar sin miedo.
Ejercicios
Todos se resuelven sobre una sola tabla. Escribe la consulta antes de mirar la solución y compara los resultados.
Ejercicio 1: Consultas de catálogo
- Los títulos y editoriales de los libros publicados antes de 2010, del más antiguo al más reciente.
- Los libros sin ISBN registrado.
- Las editoriales distintas del fondo, en orden alfabético.
- Los títulos que contienen la palabra "mar" en cualquier posición, sin distinguir mayúsculas.
- Los códigos de los ejemplares de la sucursal 2 que no estén disponibles.
- Los tres libros más recientes del fondo.
Ejercicio 2: Consultas sobre socios y préstamos
- Los socios dados de alta en 2021 o 2022, con nombre y apellidos en una sola columna llamada
socio. - Los socios que no están activos.
- Los préstamos devueltos con retraso (la fecha real de devolución es posterior a la prevista), ordenados por días de retraso… o, si no sabes calcular la diferencia todavía, simplemente por fecha de préstamo.
- Los préstamos abiertos cuya devolución estaba prevista antes del 10 de agosto de 2026.
- La segunda página de un listado de préstamos ordenado por fecha de préstamo descendente, con 5 préstamos por página.
- Los préstamos cuyo recargo no es cero, incluidos aquellos en los que el recargo aún no se ha calculado.
Ejercicio 3: Modificación segura
Escribe las instrucciones para cada operación, precedidas del SELECT de comprobación y seguidas de la instrucción que deshace el cambio.
- El ejemplar
EJ-3088sale del taller: pásalo adisponible. - Corrige la editorial del libro 337: pasa de "Ediciones Marlia" a "Ediciones Marlia SL".
- Da de alta una socia nueva, Berta Colomer (
[email protected]), en la sucursal Sur, con fecha de hoy (usa'2026-08-02'), activa, dejando que el gestor le asigne el identificador. Después, bórrala. - La reserva 4 estaba cancelada por error: reactívala.
Soluciones
Solución 1
-- 1
SELECT titulo, editorial, anio_publicacion
FROM libros
WHERE anio_publicacion < 2010
ORDER BY anio_publicacion ASC;| titulo | editorial | anio_publicacion |
|---|---|---|
| Memoria del Ensanche (1904) | Ayuntamiento de Vallmar | 1904 |
| Los pilares de la Tierra | Editorial Andana | 1989 |
| El mapa del tiempo | Editorial Andana | 2008 |
| libro_id | titulo |
|---|---|
| 339 | Memoria del Ensanche (1904) |
| editorial |
|---|
| Ayuntamiento de Vallmar |
| Ediciones Marlia |
| Editorial Andana |
| Prensa Técnica Norte |
-- 4 (ILIKE en PostgreSQL; LOWER(titulo) LIKE '%mar%' es la forma portable)
SELECT titulo FROM libros WHERE titulo ILIKE '%mar%';| titulo |
|---|
| El mapa del tiempo |
| La casa de las mareas |
Observa que "El mapa" entra por mapa y "las mareas" por mareas: LIKE busca subcadenas, no palabras completas.
| codigo | estado |
|---|---|
| EJ-3081 | prestado |
| EJ-3090 | prestado |
| titulo | anio_publicacion |
|---|---|
| Manual de jardinería urbana | 2023 |
| El invierno de los pájaros | 2021 |
| Álgebra para impacientes | 2019 |
Solución 2
-- 1
SELECT nombre || ' ' || apellidos AS socio, fecha_alta
FROM socios
WHERE fecha_alta BETWEEN '2021-01-01' AND '2022-12-31'
ORDER BY fecha_alta;| socio | fecha_alta |
|---|---|
| Marta Alsina | 2021-03-08 |
| Iván Pereda | 2021-09-19 |
| Nuria Bastos | 2022-01-30 |
-- 2
SELECT socio_id, nombre, apellidos FROM socios WHERE activo = FALSE;
-- también vale: WHERE NOT activo| socio_id | nombre | apellidos |
|---|---|---|
| 13 | Ramón | Etxebarri |
-- 3
SELECT prestamo_id, fecha_prestamo, fecha_devolucion_prevista, fecha_devolucion, recargo
FROM prestamos
WHERE fecha_devolucion > fecha_devolucion_prevista
ORDER BY fecha_prestamo;| prestamo_id | fecha_prestamo | fecha_devolucion_prevista | fecha_devolucion | recargo |
|---|---|---|---|---|
| 2 | 2026-03-05 | 2026-03-26 | 2026-04-02 | 1.40 |
| 5 | 2026-04-12 | 2026-05-03 | 2026-05-10 | 1.40 |
| 8 | 2026-05-19 | 2026-06-09 | 2026-06-30 | 4.20 |
Nótese que la condición compara dos columnas de la misma fila, algo perfectamente legítimo. Y que los cuatro préstamos abiertos no aparecen: NULL > fecha es DESCONOCIDO. En PostgreSQL, fecha_devolucion - fecha_devolucion_prevista daría directamente los días de retraso (7, 7 y 21).
-- 4
SELECT prestamo_id, socio_id, fecha_devolucion_prevista
FROM prestamos
WHERE fecha_devolucion IS NULL
AND fecha_devolucion_prevista < '2026-08-10'
ORDER BY fecha_devolucion_prevista;| prestamo_id | socio_id | fecha_devolucion_prevista |
|---|---|---|
| 9 | 14 | 2026-08-04 |
| 10 | 15 | 2026-08-08 |
-- 5 Página 2 con 5 por página: OFFSET (2-1) * 5 = 5
SELECT prestamo_id, fecha_prestamo
FROM prestamos
ORDER BY fecha_prestamo DESC, prestamo_id DESC
LIMIT 5 OFFSET 5;| prestamo_id | fecha_prestamo |
|---|---|
| 7 | 2026-05-08 |
| 4 | 2026-04-06 |
| 3 | 2026-03-11 |
| 2 | 2026-03-05 |
| 1 | 2026-03-02 |
(El segundo criterio prestamo_id DESC garantiza que la paginación sea estable aunque hubiera fechas repetidas.)
-- 6 La clave: NULL no es "distinto de cero", es "no se sabe"
SELECT prestamo_id, recargo
FROM prestamos
WHERE recargo <> 0 OR recargo IS NULL
ORDER BY prestamo_id;| prestamo_id | recargo |
|---|---|
| 2 | 1.40 |
| 5 | 1.40 |
| 8 | 4.20 |
| 9 | (NULL) |
| 10 | (NULL) |
| 11 | (NULL) |
| 12 | (NULL) |
Sin el OR recargo IS NULL habrías obtenido solo tres filas y habrías perdido de vista los cuatro préstamos en curso.
Solución 3
-- 1
SELECT ejemplar_id, codigo, estado FROM ejemplares WHERE codigo = 'EJ-3088'; -- reparacion
UPDATE ejemplares SET estado = 'disponible' WHERE codigo = 'EJ-3088'; -- UPDATE 1
UPDATE ejemplares SET estado = 'reparacion' WHERE codigo = 'EJ-3088'; -- deshacer
-- 2
SELECT libro_id, titulo, editorial FROM libros WHERE libro_id = 337;
UPDATE libros SET editorial = 'Ediciones Marlia SL' WHERE libro_id = 337; -- UPDATE 1
UPDATE libros SET editorial = 'Ediciones Marlia' WHERE libro_id = 337; -- deshacer
-- 3 Sin dar socio_id: lo genera la columna IDENTITY (será el 21 si
-- resincronizaste la secuencia en el apartado 3; si no, dará error de
-- clave duplicada, que es justamente la lección de aquel apartado).
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('Berta', 'Colomer', '[email protected]', '2026-08-02', 3, TRUE)
RETURNING socio_id;
-- socio_id
-- ----------
-- 21
SELECT socio_id, nombre, apellidos FROM socios WHERE apellidos = 'Colomer';
DELETE FROM socios WHERE apellidos = 'Colomer'; -- DELETE 1
-- 4
SELECT reserva_id, estado FROM reservas WHERE reserva_id = 4; -- cancelada
UPDATE reservas SET estado = 'activa' WHERE reserva_id = 4; -- UPDATE 1
UPDATE reservas SET estado = 'cancelada' WHERE reserva_id = 4; -- deshacerComprobación final de que todo ha quedado como estaba:
SELECT COUNT(*) FROM socios; -- 10
SELECT COUNT(*) FROM prestamos; -- 12
SELECT estado FROM reservas WHERE reserva_id = 4; -- canceladaConclusión
Esta lección ha llevado a BiblioRed de un esquema vacío a una base de datos viva y consultable:
INSERTcon lista de columnas explícita (siempre), en una o varias filas, conNULLsin comillas y conRETURNINGen PostgreSQL para conocer los identificadores generados. Y la comprobación automática de unicidad y de claves ajenas que la hoja de cálculo nunca tuvo.- El juego de datos de BiblioRed: 4 sucursales, 8 autores, 10 socios, 9 libros, 15 ejemplares, 12 préstamos y 5 reservas, todo ficticio y coherente. Es el material de trabajo del resto del curso.
- El efecto secundario de insertar claves a mano y cómo resincronizar el generador con
RESTART WITHosetval. SELECTcon proyección de columnas, aliasAS(con comillas dobles cuando llevan espacios), concatenación con||yDISTINCTaplicado a la combinación completa de columnas.WHEREcon comparaciones,AND/OR/NOTy su precedencia,BETWEEN(extremos incluidos),INyNOT IN(con su trampa conNULL),LIKE/ILIKEcon%y_, eIS NULL, que es la única forma de preguntar por la ausencia.ORDER BYcon varios criterios,ASC/DESC,NULLS FIRST/NULLS LASTy la importancia de un criterio de desempate.LIMIT/OFFSETpara paginar, siempre conORDER BY.UPDATEyDELETEcon la disciplina delSELECTprevio, la lectura del número de filas afectadas y el respeto que impone elWHEREolvidado; másTRUNCATEcomo vaciado rápido de nivel DDL.
Hasta aquí, cada consulta ha mirado una sola tabla, y eso deja preguntas sin responder: no sabemos quién tiene EJ-3081, ni qué título es el más prestado, ni qué socios no han cogido nunca nada. Los datos están repartidos a propósito —esa es la esencia del modelo relacional— y ahora toca recomponerlos.
En la lección 02-04, Consultas Multitabla: JOIN y Subconsultas, aprenderás a unir socios con prestamos, prestamos con ejemplares, ejemplares con libros y libros con autores, todo en una sola consulta; a usar LEFT JOIN para encontrar lo que no tiene correspondencia; y a anidar consultas dentro de consultas. Es donde SQL empieza a resultar realmente potente.
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
