Els dos terminals estan oberts i l'esquema de BiblioRed el tens a mà, tal com vam quedar en tancar el mòdul 6. A partir d'aquí el curs canvia de registre: no llegiràs conceptes nous, escriuràs SQL. Aquesta lliçó és un banc de quinze exercicis sobre la xarxa de biblioteques municipals de Vallmar, ordenats per dificultat creixent, i té una manera d'usar-se molt concreta.
Com treballar aquesta lliçó. Llegeix l'enunciat. Abans de mirar res més, escriu la teva consulta a la sessió psql i executa-la. Compara el teu resultat amb el bloc Resultat esperat. Només llavors desplega la Solució i compara-la amb la teva: és perfectament normal —i sovint desitjable— que la teva consulta sigui diferent de la proposada i doni el mateix resultat, perquè en SQL gairebé sempre hi ha més d'un camí. El que no és normal és llegir la solució primer: en aquest cas l'exercici no t'ensenya res, només et dóna la sensació d'haver entès.
Si una consulta no et surt, resisteix almenys cinc minuts abans de mirar la Pista. El múscul que estàs entrenant aquí no és el de recordar la sintaxi de HAVING, sinó el de traduir una pregunta en llenguatge humà a una consulta, i això només s'entrena fallant.
Aquesta lliçó cobreix SELECT, WHERE, JOIN, agregació i subconsultes. Les funcions de finestra, les transaccions i els plans d'execució no hi apareixen: són el material de la lliçó 07-04.
Abans de Començar
Els exercicis fan servir un joc de dades concret i petit —8 socis, 12 materials, 15 exemplars, 20 préstecs— perquè puguis verificar cada resultat a ull. Si has anat construint BiblioRed al llarg del curs, les teves dades seran diferents i els resultats no coincidiran. Per això convé partir de zero amb l'script d'aquesta lliçó.
Script de creació i càrrega
Crea una base de dades neta i executa aquest script sencer. A PostgreSQL:
-- ============================================================
-- BiblioRed - joc de dades reduït per al mòdul 7
-- PostgreSQL 14+. Totes les dades són fictícies.
-- ============================================================
DROP TABLE IF EXISTS pagaments, multes, inscripcions, participacions, ponents,
esdeveniments, tipus_esdeveniment, sales, reserves, prestecs,
exemplars, materials, telefons_soci, socis,
autors, sucursals CASCADE;
CREATE TABLE sucursals (
sucursal_id INTEGER PRIMARY KEY,
nom VARCHAR(60) NOT NULL UNIQUE,
adr_carrer VARCHAR(80),
adr_numero VARCHAR(10),
adr_codi_postal CHAR(5),
adr_ciutat VARCHAR(60) NOT NULL DEFAULT 'Vallmar',
telefon VARCHAR(15),
data_obertura DATE
);
CREATE TABLE autors (
autor_id INTEGER PRIMARY KEY,
nom VARCHAR(60) NOT NULL,
cognoms VARCHAR(80) NOT NULL,
nacionalitat VARCHAR(40),
any_naixement SMALLINT
);
CREATE TABLE socis (
soci_id INTEGER PRIMARY KEY,
nom VARCHAR(60) NOT NULL,
cognoms VARCHAR(80) NOT NULL,
email VARCHAR(120) NOT NULL UNIQUE,
data_alta DATE NOT NULL,
sucursal_id INTEGER NOT NULL REFERENCES sucursals(sucursal_id),
actiu BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE telefons_soci (
soci_id INTEGER NOT NULL REFERENCES socis(soci_id) ON DELETE CASCADE,
numero VARCHAR(15) NOT NULL,
tipus VARCHAR(10) NOT NULL,
PRIMARY KEY (soci_id, numero)
);
CREATE TABLE materials (
material_id INTEGER PRIMARY KEY,
tipus_material VARCHAR(15) NOT NULL,
titol VARCHAR(200) NOT NULL,
autor_id INTEGER REFERENCES autors(autor_id),
editorial VARCHAR(80),
any_publicacio SMALLINT,
idioma CHAR(2) NOT NULL DEFAULT 'ca',
data_alta DATE NOT NULL,
portada_url VARCHAR(200)
);
CREATE TABLE exemplars (
exemplar_id INTEGER PRIMARY KEY,
codi VARCHAR(15) NOT NULL UNIQUE,
material_id INTEGER NOT NULL REFERENCES materials(material_id),
num_exemplar SMALLINT NOT NULL,
sucursal_id INTEGER NOT NULL REFERENCES sucursals(sucursal_id),
estat VARCHAR(15) NOT NULL,
data_adquisicio DATE
);
CREATE TABLE prestecs (
prestec_id INTEGER PRIMARY KEY,
soci_id INTEGER NOT NULL REFERENCES socis(soci_id),
exemplar_id INTEGER NOT NULL REFERENCES exemplars(exemplar_id),
data_prestec DATE NOT NULL,
data_devolucio_prevista DATE NOT NULL,
data_devolucio DATE
);
CREATE TABLE reserves (
reserva_id INTEGER PRIMARY KEY,
soci_id INTEGER NOT NULL REFERENCES socis(soci_id),
material_id INTEGER NOT NULL REFERENCES materials(material_id),
data_reserva DATE NOT NULL,
data_expiracio DATE,
estat VARCHAR(15) NOT NULL
);
CREATE TABLE sales (
sala_id INTEGER PRIMARY KEY,
sucursal_id INTEGER NOT NULL REFERENCES sucursals(sucursal_id),
nom VARCHAR(60) NOT NULL,
aforament SMALLINT NOT NULL,
planta SMALLINT NOT NULL,
accessible BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE (sucursal_id, nom)
);
CREATE TABLE tipus_esdeveniment (
tipus_esdeveniment_id INTEGER PRIMARY KEY,
codi VARCHAR(10) NOT NULL UNIQUE,
nom VARCHAR(60) NOT NULL,
descripcio TEXT,
durada_estandard_min SMALLINT,
actiu BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE esdeveniments (
esdeveniment_id INTEGER PRIMARY KEY,
titol VARCHAR(150) NOT NULL,
descripcio TEXT,
tipus_esdeveniment_id INTEGER NOT NULL REFERENCES tipus_esdeveniment(tipus_esdeveniment_id),
sala_id INTEGER NOT NULL REFERENCES sales(sala_id),
inici TIMESTAMP NOT NULL,
fi TIMESTAMP NOT NULL,
places_ofertes SMALLINT NOT NULL,
estat VARCHAR(15) NOT NULL,
publicat BOOLEAN NOT NULL DEFAULT FALSE,
versio INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE inscripcions (
esdeveniment_id INTEGER NOT NULL REFERENCES esdeveniments(esdeveniment_id),
soci_id INTEGER NOT NULL REFERENCES socis(soci_id),
data_inscripcio DATE NOT NULL,
estat VARCHAR(15) NOT NULL,
acompanyants SMALLINT NOT NULL DEFAULT 0,
places_ocupades SMALLINT NOT NULL DEFAULT 1,
PRIMARY KEY (esdeveniment_id, soci_id)
);
CREATE TABLE ponents (
ponent_id INTEGER PRIMARY KEY,
nom VARCHAR(60) NOT NULL,
cognoms VARCHAR(80) NOT NULL,
email VARCHAR(120) NOT NULL UNIQUE,
biografia TEXT,
extern BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE participacions (
esdeveniment_id INTEGER NOT NULL REFERENCES esdeveniments(esdeveniment_id),
ponent_id INTEGER NOT NULL REFERENCES ponents(ponent_id),
rol VARCHAR(30) NOT NULL,
honoraris NUMERIC(8,2) NOT NULL DEFAULT 0,
PRIMARY KEY (esdeveniment_id, ponent_id, rol)
);
CREATE TABLE multes (
multa_id INTEGER PRIMARY KEY,
soci_id INTEGER NOT NULL REFERENCES socis(soci_id),
prestec_id INTEGER REFERENCES prestecs(prestec_id),
motiu VARCHAR(15) NOT NULL,
import NUMERIC(8,2) NOT NULL,
data_emissio DATE NOT NULL,
estat VARCHAR(15) NOT NULL
);
CREATE TABLE pagaments (
pagament_id INTEGER PRIMARY KEY,
multa_id INTEGER NOT NULL REFERENCES multes(multa_id),
data_pagament DATE NOT NULL,
import NUMERIC(8,2) NOT NULL,
metode VARCHAR(15) NOT NULL,
referencia VARCHAR(30)
);
-- ---------------- DADES ----------------
INSERT INTO sucursals VALUES
(1,'Centre','Plaça Major','1','08130','Vallmar','935550001','1988-05-12'),
(2,'Nord','Avinguda del Bosc','44','08131','Vallmar','935550002','2001-09-03'),
(3,'Sud','Carrer Marina','7','08132','Vallmar','935550003','2010-01-18'),
(4,'Est','Ronda de Llevant','120','08133','Vallmar','935550004','2019-11-25');
INSERT INTO autors VALUES
(1,'Ken','Follett','britànica',1949),
(2,'Félix J.','Palma','espanyola',1968),
(3,'Almudena','Grandes','espanyola',1960),
(4,'Haruki','Murakami','japonesa',1949),
(5,'Svetlana','Aleksiévitx','bielorussa',1948),
(6,'Delia','Marchetti','argentina',NULL);
INSERT INTO socis VALUES
(11,'Clara','Ferran','[email protected]','2024-03-12',1,TRUE),
(12,'Dídac','Rovira','[email protected]','2024-06-01',2,TRUE),
(13,'Sònia','Mestre','[email protected]','2025-01-20',1,TRUE),
(14,'Marta','Alsina','[email protected]','2025-02-14',1,TRUE),
(15,'Ivan','Pereda','[email protected]','2025-04-03',2,TRUE),
(16,'Núria','Bastos','[email protected]','2025-09-30',3,TRUE),
(17,'Òscar','Vilanova','[email protected]','2026-01-15',4,FALSE),
(18,'Berta','Quintana','[email protected]','2026-02-08',3,TRUE);
INSERT INTO telefons_soci VALUES
(11,'931222333','fix'),(14,'600111222','mobil'),
(14,'931000111','fix'),(15,'600333444','mobil'),(16,'600555666','mobil');
INSERT INTO materials VALUES
(901,'llibre','Els pilars de la Terra',1,'Plaza & Janés',1989,'ca','2024-01-10','/img/901.jpg'),
(902,'llibre','El mapa del temps',2,'Algaida',2008,'ca','2024-01-10','/img/902.jpg'),
(903,'llibre','Un món sense fi',1,'Plaza & Janés',2007,'ca','2024-02-05',NULL),
(904,'llibre','El cor gelat',3,'Tusquets',2007,'ca','2024-03-01','/img/904.jpg'),
(905,'llibre','Tokio blues',4,'Tusquets',1987,'ca','2024-03-01',NULL),
(906,'llibre','Kafka a la platja',4,'Tusquets',2002,'ca','2025-01-12','/img/906.jpg'),
(907,'llibre','La capsa dels desitjos',6,'Editorial Vallmar',2019,'ca','2025-02-20',NULL),
(908,'dvd','Els pilars de la Terra (sèrie)',1,'Sono Media',2010,'ca','2025-03-15','/img/908.jpg'),
(909,'dvd','Documental: la veu de Txernòbil',5,'Filmin',2019,'ca','2025-04-02',NULL),
(910,'revista','Ciència Vallmar 42',NULL,'Ajuntament de Vallmar',2026,'ca','2026-01-08',NULL),
(911,'revista','Ciència Vallmar 43',NULL,'Ajuntament de Vallmar',2026,'ca','2026-02-08',NULL),
(912,'audiollibre','Veus de Txernòbil',5,'Debate Audio',2015,'ca','2025-06-11','/img/912.jpg');
INSERT INTO exemplars VALUES
(3081,'EJ-3081',902,1,1,'prestat','2024-01-15'),
(3082,'EJ-3082',902,2,2,'disponible','2024-01-15'),
(3083,'EJ-3083',901,1,1,'disponible','2024-01-15'),
(3084,'EJ-3084',901,2,3,'disponible','2024-01-15'),
(3085,'EJ-3085',901,3,4,'reservat','2025-02-01'),
(3086,'EJ-3086',903,1,1,'disponible','2024-02-10'),
(3087,'EJ-3087',904,1,2,'prestat','2024-03-05'),
(3088,'EJ-3088',905,1,1,'prestat','2024-03-05'),
(3089,'EJ-3089',906,1,3,'disponible','2025-01-20'),
(3090,'EJ-3090',906,2,1,'disponible','2025-01-20'),
(3091,'EJ-3091',908,1,1,'prestat','2025-03-20'),
(3092,'EJ-3092',909,1,2,'disponible','2025-04-10'),
(3093,'EJ-3093',912,1,1,'extraviat','2025-06-15'),
(3094,'EJ-3094',910,1,1,'disponible','2026-01-10'),
(3095,'EJ-3095',904,2,4,'retirat','2024-03-05');
INSERT INTO prestecs VALUES
(1,14,3081,'2026-01-10','2026-01-31','2026-01-28'),
(2,14,3083,'2026-02-02','2026-02-23','2026-03-05'),
(3,15,3082,'2026-02-10','2026-03-03','2026-02-28'),
(4,16,3084,'2026-02-15','2026-03-08','2026-03-08'),
(5,11,3086,'2026-03-01','2026-03-22','2026-03-20'),
(6,12,3087,'2026-03-05','2026-03-26','2026-04-10'),
(7,14,3089,'2026-03-12','2026-04-02','2026-03-30'),
(8,15,3090,'2026-04-01','2026-04-22','2026-04-20'),
(9,13,3088,'2026-04-05','2026-04-26',NULL),
(10,16,3091,'2026-04-20','2026-05-11','2026-05-09'),
(11,11,3092,'2026-05-02','2026-05-23','2026-05-21'),
(12,14,3093,'2026-05-10','2026-05-31',NULL),
(13,12,3081,'2026-05-15','2026-06-05','2026-06-03'),
(14,15,3083,'2026-06-01','2026-06-22','2026-06-30'),
(15,16,3087,'2026-06-10','2026-07-01',NULL),
(16,13,3089,'2026-06-15','2026-07-06','2026-07-04'),
(17,14,3091,'2026-07-20','2026-08-10',NULL),
(18,11,3082,'2026-07-05','2026-07-26','2026-07-24'),
(19,15,3081,'2026-07-22','2026-08-12',NULL),
(20,12,3086,'2026-07-10','2026-07-31','2026-07-29');
INSERT INTO reserves VALUES
(501,14,901,'2026-01-05','2026-01-19','atesa'),
(502,15,902,'2026-07-18','2026-08-01','activa'),
(503,16,906,'2026-06-01','2026-06-15','expirada'),
(504,11,908,'2026-07-25','2026-08-08','activa'),
(505,13,902,'2026-05-02','2026-05-16','cancellada'),
(506,12,901,'2026-03-30','2026-04-13','expirada');
INSERT INTO sales VALUES
(1,1,'Auditori',120,0,TRUE),(2,1,'Sala Blava',30,1,TRUE),
(3,2,'Sala Nord',40,0,TRUE),(4,3,'Aula Sud',25,1,FALSE),
(5,4,'Sala Est',50,0,TRUE);
INSERT INTO tipus_esdeveniment VALUES
(1,'CLUB','Club de lectura',NULL,90,TRUE),
(2,'CONTE','Contacontes infantil',NULL,45,TRUE),
(3,'TALLER','Taller',NULL,120,TRUE),
(4,'PRES','Presentació de llibre',NULL,60,TRUE);
INSERT INTO esdeveniments VALUES
(101,'Club de lectura: Els pilars de la Terra',NULL,1,2,'2026-03-12 18:00','2026-03-12 19:30',12,'celebrat',TRUE,3),
(102,'Contacontes de primavera',NULL,2,3,'2026-04-18 11:00','2026-04-18 11:45',20,'celebrat',TRUE,2),
(103,'Taller d''escriptura creativa',NULL,3,4,'2026-05-09 17:00','2026-05-09 19:00',8,'celebrat',TRUE,4),
(104,'Presentació: La capsa dels desitjos',NULL,4,1,'2026-06-04 19:00','2026-06-04 20:00',40,'celebrat',TRUE,2),
(105,'Club de lectura: Tokio blues',NULL,1,5,'2026-07-16 18:00','2026-07-16 19:30',10,'celebrat',TRUE,3),
(106,'Taller d''iniciació a la genealogia',NULL,3,2,'2026-09-10 17:00','2026-09-10 19:00',15,'obert',TRUE,1),
(107,'Contacontes de tardor',NULL,2,3,'2026-10-03 11:00','2026-10-03 11:45',20,'programat',FALSE,1);
INSERT INTO inscripcions VALUES
(101,14,'2026-02-20','assistida',1,2),(101,11,'2026-02-22','assistida',0,1),
(101,15,'2026-03-01','cancellada',0,1),(101,12,'2026-03-02','assistida',0,1),
(101,13,'2026-03-03','assistida',2,3),
(102,16,'2026-04-01','assistida',3,4),(102,12,'2026-04-02','assistida',2,3),
(102,11,'2026-04-05','confirmada',1,2),(102,15,'2026-04-06','cancellada',0,1),
(103,14,'2026-04-20','assistida',0,1),(103,13,'2026-04-21','assistida',0,1),
(103,15,'2026-04-22','assistida',1,2),(103,16,'2026-04-25','llista_espera',0,1),
(103,11,'2026-04-26','confirmada',0,1),
(104,11,'2026-05-10','assistida',1,2),(104,12,'2026-05-11','assistida',0,1),
(104,14,'2026-05-12','assistida',3,4),(104,16,'2026-05-14','assistida',0,1),
(104,13,'2026-05-15','cancellada',0,1),
(105,15,'2026-06-30','assistida',0,1),(105,16,'2026-07-01','assistida',1,2),
(105,14,'2026-07-02','assistida',0,1),(105,11,'2026-07-03','llista_espera',0,1),
(106,14,'2026-07-28','confirmada',1,2),(106,15,'2026-07-29','confirmada',0,1),
(106,16,'2026-07-30','confirmada',0,1);
INSERT INTO ponents VALUES
(1,'Rosa','Calduch','[email protected]',NULL,FALSE),
(2,'Aitor','Lemus','[email protected]',NULL,TRUE),
(3,'Delia','Marchetti','[email protected]',NULL,TRUE);
INSERT INTO participacions VALUES
(101,1,'moderadora',0),(102,2,'narrador',180.00),(103,2,'tallerista',350.00),
(104,3,'autora',250.00),(104,1,'presentadora',0),(105,1,'moderadora',0),
(106,2,'tallerista',300.00);
INSERT INTO multes VALUES
(1,14,2,'retard',2.20,'2026-03-05','pagada'),
(2,12,6,'retard',3.00,'2026-04-10','pagada'),
(3,15,14,'retard',1.60,'2026-06-30','pendent'),
(4,14,12,'perdua',24.00,'2026-06-01','pendent'),
(5,16,10,'deteriorament',6.50,'2026-05-09','pagada'),
(6,11,5,'deteriorament',4.00,'2026-03-20','condonada'),
(7,13,9,'retard',5.00,'2026-07-20','pendent'),
(8,16,15,'retard',3.20,'2026-07-25','anullada');
INSERT INTO pagaments VALUES
(1,1,'2026-03-06',2.20,'efectiu','CAIXA-C-0341'),
(2,2,'2026-04-12',3.00,'targeta','TPV-000912'),
(3,5,'2026-05-10',4.00,'targeta','TPV-001033'),
(4,5,'2026-05-18',2.50,'passarella','PSG-77120');Comprovació que les dades estan carregades
Executa aquesta consulta abans de començar. Si algun número no coincideix, l'script no s'ha executat sencer:
SELECT 'sucursals' AS taula, count(*) FROM sucursals
UNION ALL SELECT 'autors', count(*) FROM autors
UNION ALL SELECT 'socis', count(*) FROM socis
UNION ALL SELECT 'materials', count(*) FROM materials
UNION ALL SELECT 'exemplars', count(*) FROM exemplars
UNION ALL SELECT 'prestecs', count(*) FROM prestecs
UNION ALL SELECT 'reserves', count(*) FROM reserves
UNION ALL SELECT 'esdeveniments', count(*) FROM esdeveniments
UNION ALL SELECT 'inscripcions', count(*) FROM inscripcions
UNION ALL SELECT 'multes', count(*) FROM multes
UNION ALL SELECT 'pagaments', count(*) FROM pagaments
ORDER BY 1;| taula | count |
|---|---|
| autors | 6 |
| esdeveniments | 7 |
| exemplars | 15 |
| inscripcions | 26 |
| materials | 12 |
| multes | 8 |
| pagaments | 4 |
| prestecs | 20 |
| reserves | 6 |
| socis | 8 |
| sucursals | 4 |
La data de referència. Diversos exercicis calculen retards. Perquè els resultats siguin reproduïbles avui i d'aquí a dos anys, les solucions fan servir la data literal DATE '2026-08-02' en lloc de CURRENT_DATE. En producció escriuries CURRENT_DATE; aquí necessitem que el nombre de dies no canviï.
SQLite. El joc de dades funciona a SQLite canviant SERIAL/BOOLEAN per enters i NUMERIC(8,2) per REAL. Allà on hi hagi diferències rellevants en una consulta, la solució ho indica.
Contingut
- Bloc A — Bàsic: consultes sobre una sola taula (exercicis 1-3)
- Bloc B — Intermedi: consultes sobre diverses taules (exercicis 4-7)
- Bloc C — Intermedi: agregació i agrupació (exercicis 8-10)
- Bloc D — Avançat: subconsultes, taules derivades i CTE (exercicis 11-12)
- Bloc E — Avançat: informes de gestió reals (exercicis 13-15)
- Errors habituals i consells
- Exercicis de reforç
Bloc A — Bàsic: una sola taula
Exercici 1: Projecció, àlies i ordenació
Dificultat: Bàsic
Enunciat. El servei de patrimoni vol el llistat dels cinc materials més antics del catàleg. Retorna tres columnes amb aquestes capçaleres exactes: Títol, Editorial i Any. Ordena de més antic a més modern i, quan dos materials siguin del mateix any, alfabèticament per títol.
Solució
SELECT titol AS "Títol",
editorial AS "Editorial",
any_publicacio AS "Any"
FROM materials
ORDER BY any_publicacio ASC, titol ASC
LIMIT 5;Resultat esperat
| Títol | Editorial | Any |
|---|---|---|
| Tokio blues | Tusquets | 1987 |
| Els pilars de la Terra | Plaza & Janés | 1989 |
| Kafka a la platja | Tusquets | 2002 |
| El cor gelat | Tusquets | 2007 |
| Un món sense fi | Plaza & Janés | 2007 |
Explicació. Hi ha tres detalls que separen una consulta correcta d'una consulta descurada:
- Les cometes dobles als àlies.
AS Títolfallaria o es convertiria entítolen minúscules, perquè PostgreSQL passa a minúscules tot identificador sense cometes. Per conservar la majúscula i l'accent calen cometes dobles, no simples: les cometes simples delimiten cadenes de text, no identificadors.AS 'Títol'és un error de sintaxi. - El segon criteri d'ordenació. Sense
, titol ASC, les dues files de 2007 sortirien en un ordre que el motor no garanteix. Pot semblar-te estable avui i canviar demà en afegir una fila o en crear un índex. UnORDER BYque no desempata no és determinista, i en un llistat ambLIMITaixò significa que la fila que veus pot variar entre execucions. LIMITva després d'ORDER BY, no abans: primer s'ordena el conjunt sencer, després es tallen cinc files. Si ho fessis a l'inrevés obtindries cinc files qualssevol i després ordenades, que és una pregunta diferent.
A SQLite la consulta funciona igual, però els àlies entre cometes es comporten de manera més laxa (accepta cometes dobles i també claudàtors).
Exercici 2: Filtres amb BETWEEN, IN, LIKE, IS NULL i DISTINCT
Dificultat: Bàsic
Enunciat. Resol les cinc preguntes següents, cadascuna amb la seva pròpia consulta:
- (a) Materials publicats entre 2000 i 2010, tots dos inclosos, amb títol i any.
- (b) Exemplars l'estat dels quals sigui
prestatoreservat, amb codi, estat i sucursal. - (c) Materials el títol dels quals contingui la paraula «Txernòbil», sense importar majúscules ni minúscules.
- (d) Materials sense portada (
portada_urlno informada) i autors sense any de naixement. - (e) Els tipus de material diferents que hi ha al catàleg i quantes editorials diferents hi apareixen.
Pista. Per a (c), a PostgreSQL existeix ILIKE; per a (d), recorda que = NULL mai no és cert.
Solució
-- (a) BETWEEN és inclusiu pels dos extrems
SELECT titol, any_publicacio
FROM materials
WHERE any_publicacio BETWEEN 2000 AND 2010
ORDER BY any_publicacio, titol;
-- (b) IN substitueix una cadena d'OR
SELECT e.codi, e.estat, s.nom AS sucursal
FROM exemplars e
JOIN sucursals s ON s.sucursal_id = e.sucursal_id
WHERE e.estat IN ('prestat', 'reservat')
ORDER BY e.codi;
-- (c) ILIKE = LIKE insensible a majúscules (PostgreSQL)
SELECT material_id, titol
FROM materials
WHERE titol ILIKE '%txernòbil%'
ORDER BY material_id;
-- (d) IS NULL, mai = NULL
SELECT material_id, titol FROM materials WHERE portada_url IS NULL ORDER BY material_id;
SELECT autor_id, nom, cognoms FROM autors WHERE any_naixement IS NULL;
-- (e) DISTINCT i COUNT(DISTINCT ...)
SELECT DISTINCT tipus_material FROM materials ORDER BY tipus_material;
SELECT count(DISTINCT editorial) AS editorials FROM materials;Resultat esperat
(a) 5 files: Kafka a la platja (2002), El cor gelat (2007), Un món sense fi (2007), El mapa del temps (2008), Els pilars de la Terra (sèrie) (2010).
(b)
| codi | estat | sucursal |
|---|---|---|
| EJ-3081 | prestat | Centre |
| EJ-3085 | reservat | Est |
| EJ-3087 | prestat | Nord |
| EJ-3088 | prestat | Centre |
| EJ-3091 | prestat | Centre |
(c) 2 files: 909 «Documental: la veu de Txernòbil» i 912 «Veus de Txernòbil».
(d) 6 materials sense portada (903, 905, 907, 909, 910, 911) i 1 autora sense any de naixement (Delia Marchetti).
(e) 4 tipus (audiollibre, dvd, llibre, revista) i 8 editorials diferents.
Explicació. Cada apartat amaga un parany clàssic:
BETWEEN 2000 AND 2010inclou els dos extrems. Si necessites excloure'ls,BETWEENno serveix: cal escriure> 2000 AND < 2010. I l'ordre importa:BETWEEN 2010 AND 2000retorna zero files, sense error i sense avís.IN ('prestat','reservat')és exactament equivalent aestat = 'prestat' OR estat = 'reservat'. És més llegible i, sobretot, evita l'error de precedència d'escriureWHERE material_id = 901 AND estat = 'prestat' OR estat = 'reservat', que no significa el que sembla:ANDlliga més fort queOR.ILIKEés una extensió de PostgreSQL. A SQLite,LIKEja és insensible a majúscules per als caràcters ASCII, però no per als accents ni per a laÒde «Txernòbil»; allà el que és portable ésWHERE lower(titol) LIKE lower('%txernòbil%').portada_url = NULLretornaNULL, que en unWHEREes comporta com a fals: la consulta no dóna error, dóna zero files. És una de les errades més difícils de detectar perquè no hi ha cap símptoma.count(DISTINCT editorial)compta 8 i no 9: hi ha 12 materials però només 8 editorials diferents, icount(DISTINCT ...)ignora els NULL (aquí no hi ha editorials nul·les, però convé recordar-ho).
Exercici 3: INSERT, UPDATE i DELETE amb la disciplina del SELECT previ
Dificultat: Bàsic
Enunciat. Tres operacions de manteniment. En totes tres, abans de modificar res, escriu i executa el SELECT amb el mateix WHERE per veure exactament quines files tocaràs.
- (a) Dóna d'alta el soci 19, Ruben Ortells, correu
[email protected], alta avui (2026-08-02), sucursal Est, actiu. - (b) La sòcia 18 (Berta Quintana) es trasllada de la sucursal Sud a la sucursal Nord. Actualitza la seva sucursal.
- (c) Esborra les reserves expirades la data d'expiració de les quals sigui anterior al 30 de juny de 2026.
Solució
-- (a) INSERT amb llista de columnes explícita
INSERT INTO socis (soci_id, nom, cognoms, email, data_alta, sucursal_id, actiu)
VALUES (19, 'Ruben', 'Ortells', '[email protected]', DATE '2026-08-02', 4, TRUE);
-- (b) Primer mirar...
SELECT soci_id, nom, cognoms, sucursal_id FROM socis WHERE soci_id = 18;
-- ...i només llavors modificar
UPDATE socis
SET sucursal_id = 2
WHERE soci_id = 18
RETURNING soci_id, cognoms, sucursal_id;
-- (c) Primer mirar...
SELECT reserva_id, soci_id, material_id, data_expiracio, estat
FROM reserves
WHERE estat = 'expirada' AND data_expiracio < DATE '2026-06-30';
-- ...i només llavors esborrar
DELETE FROM reserves
WHERE estat = 'expirada' AND data_expiracio < DATE '2026-06-30'
RETURNING reserva_id;Resultat esperat
INSERT 0 1
soci_id | cognoms | sucursal_id
---------+----------+-------------
18 | Quintana | 2
UPDATE 1
reserva_id
------------
503
506
DELETE 2Explicació. Tres hàbits que eviten gairebé tots els desastres d'un taulell:
- Llista de columnes explícita a l'
INSERT.INSERT INTO socis VALUES (...)funciona fins al dia que algú afegeix una columna asocis; llavors tots elsINSERTsense llista es trenquen o, pitjor, col·loquen els valors a la columna equivocada. - El
SELECTamb el mateixWHERE, primer. No és una recomanació d'estil: és l'única manera de saber quantes files tocaràs abans de tocar-les. UnUPDATE socis SET sucursal_id = 2senseWHEREafecta els vuit socis i no hi ha «desfer» fora d'una transacció. RETURNING(PostgreSQL) retorna les files realment afectades. És la confirmació posterior que elWHEREva fer el que estava previst. SQLite ho admet des de la versió 3.35; a MySQL no existeix i cal tornar a consultar.
Sobre (c): si intentessis esborrar la reserva 501 (atesa), no fallaria res perquè cap taula no apunta a reserves. Però si intentessis esborrar la sòcia 14 sí que fallaria, perquè prestecs, reserves, multes i inscripcions la referencien. És la integritat referencial del mòdul 2 fent la seva feina.
Desfés els canvis abans de continuar, perquè els resultats posteriors coincideixin:
DELETE FROM socis WHERE soci_id = 19; UPDATE socis SET sucursal_id = 3 WHERE soci_id = 18;INSERT INTO reserves VALUES (503,16,906,'2026-06-01','2026-06-15','expirada'), (506,12,901,'2026-03-30','2026-04-13','expirada');
Bloc B — Intermedi: diverses taules
Exercici 4: INNER JOIN de dues taules
Dificultat: Intermedi
Enunciat. Un soci pregunta al taulell on pot trobar «Els pilars de la Terra» (material 901). Retorna tots els seus exemplars amb el codi, l'estat i el nom de la sucursal on és cadascun, ordenats per sucursal.
Pista. La sucursal és a exemplars.sucursal_id, no a materials.
Solució
SELECT e.codi,
e.num_exemplar,
e.estat,
s.nom AS sucursal
FROM exemplars e
INNER JOIN sucursals s ON s.sucursal_id = e.sucursal_id
WHERE e.material_id = 901
ORDER BY s.nom;Resultat esperat
| codi | num_exemplar | estat | sucursal |
|---|---|---|---|
| EJ-3083 | 1 | disponible | Centre |
| EJ-3085 | 3 | reservat | Est |
| EJ-3084 | 2 | disponible | Sud |
Explicació. El JOIN connecta dues taules per la parella clau forana → clau primària: exemplars.sucursal_id apunta a sucursals.sucursal_id. Els àlies e i s no són decoratius: així que hi hagi dues taules amb una columna del mateix nom —i aquí sucursal_id és a totes dues— escriure sucursal_id a seques produeix l'error column reference "sucursal_id" is ambiguous.
L'error fàcil aquí és oblidar la condició de l'ON. Un FROM exemplars e, sucursals s WHERE e.material_id = 901 sense condició de reunió produeix un producte cartesià: 3 exemplars × 4 sucursals = 12 files, totes amb aspecte plausible. És un error que no salta a la vista amb tres files i que amb 40.000 exemplars tomba el servidor.
Exercici 5: La cadena de quatre taules
Dificultat: Intermedi
Enunciat. Llista tots els préstecs oberts (els que encara no s'han retornat) amb: nom i cognoms del soci, codi de l'exemplar, títol del material, sucursal a la qual pertany l'exemplar i data de devolució prevista. Ordena per data prevista.
Pista. La cadena és socis → prestecs → exemplars → materials, i la sucursal penja d'exemplars.
Solució
SELECT so.nom || ' ' || so.cognoms AS soci,
e.codi,
m.titol,
su.nom AS sucursal,
p.data_devolucio_prevista AS prevista
FROM prestecs p
INNER JOIN socis so ON so.soci_id = p.soci_id
INNER JOIN exemplars e ON e.exemplar_id = p.exemplar_id
INNER JOIN materials m ON m.material_id = e.material_id
INNER JOIN sucursals su ON su.sucursal_id = e.sucursal_id
WHERE p.data_devolucio IS NULL
ORDER BY p.data_devolucio_prevista;Resultat esperat
| soci | codi | titol | sucursal | prevista |
|---|---|---|---|---|
| Sònia Mestre | EJ-3088 | Tokio blues | Centre | 2026-04-26 |
| Marta Alsina | EJ-3093 | Veus de Txernòbil | Centre | 2026-05-31 |
| Núria Bastos | EJ-3087 | El cor gelat | Nord | 2026-07-01 |
| Marta Alsina | EJ-3091 | Els pilars de la Terra (sèrie) | Centre | 2026-08-10 |
| Ivan Pereda | EJ-3081 | El mapa del temps | Centre | 2026-08-12 |
Explicació. Quatre JOIN encadenats no són més difícils que un: cada JOIN afegeix una taula i la seva condició, i el resultat acumulat va creixent cap a la dreta. La clau és començar per la taula que conté les files que vols comptar —aquí prestecs, perquè una fila del resultat és un préstec— i penjar-hi les altres com a decoració.
Dues observacions:
p.data_devolucio IS NULLés la definició de «préstec obert» en aquest esquema. No hi ha cap columnaobert; el fet de no tenir data de devolució és el fet d'estar obert. És un ús deNULLlegítim: significa «encara no ha passat».- La sucursal ve d'
exemplars, no desocis. La Marta Alsina està adscrita a Centre i els seus dos préstecs són d'exemplars de Centre, així que la diferència no es nota; però si hagués agafat en préstec un exemplar de Nord,su.nomdiria Nord. Confondre «la sucursal del soci» amb «la sucursal de l'exemplar» és l'error de model més freqüent en aquest esquema. A SQLite,||concatena igual que a PostgreSQL.
Exercici 6: LEFT JOIN i l'antireunió amb IS NULL
Dificultat: Intermedi
Enunciat. Dues preguntes de la memòria anual:
- (a) Quins materials no s'han prestat mai? Inclou-hi també els que ni tan sols tenen exemplars.
- (b) Quins socis no tenen cap préstec registrat? Indica si estan actius.
Pista. Un LEFT JOIN seguit de WHERE <columna de la taula dreta> IS NULL deixa exactament les files que no van trobar parella.
Solució
-- (a) Materials mai prestats (dos LEFT JOIN encadenats)
SELECT m.material_id, m.titol, m.tipus_material
FROM materials m
LEFT JOIN exemplars e ON e.material_id = m.material_id
LEFT JOIN prestecs p ON p.exemplar_id = e.exemplar_id
WHERE p.prestec_id IS NULL
ORDER BY m.material_id;
-- (b) Socis sense préstecs
SELECT so.soci_id, so.nom, so.cognoms, so.actiu
FROM socis so
LEFT JOIN prestecs p ON p.soci_id = so.soci_id
WHERE p.prestec_id IS NULL
ORDER BY so.soci_id;Resultat esperat
(a)
| material_id | titol | tipus_material |
|---|---|---|
| 907 | La capsa dels desitjos | llibre |
| 910 | Ciència Vallmar 42 | revista |
| 911 | Ciència Vallmar 43 | revista |
(b)
| soci_id | nom | cognoms | actiu |
|---|---|---|---|
| 17 | Òscar | Vilanova | f |
| 18 | Berta | Quintana | t |
Explicació. El patró d'antireunió té tres parts que cal respectar juntes:
LEFT JOIN, noINNER JOIN. L'INNERdescarta precisament les files que busquem.- La condició de l'aparellament va a l'
ON. - El filtre
IS NULLva alWHEREi ha d'apuntar a una columna de la taula dreta que mai no sigui nul·la per si mateixa: per això fem servirp.prestec_id(clau primària, mai nul·la) i nop.data_devolucio, que és nul·la als préstecs oberts i ens colaria cinc falsos positius.
A (a) el segon LEFT JOIN és imprescindible. Els materials 907 i 911 no tenen exemplars: després del primer LEFT JOIN, e.exemplar_id ja és NULL, i el segon LEFT JOIN propaga aquest NULL a p.prestec_id, de manera que entren al resultat. Si haguessis fet servir INNER JOIN exemplars, aquests dos materials haurien desaparegut i l'informe hauria dit que només hi ha un material sense prestar. L'antireunió no falla amb error: falla donant menys files del compte.
El material 910 és diferent: sí que té exemplar (EJ-3094), però aquest exemplar no ha sortit mai. Els tres casos —sense exemplars i amb exemplars sense sortida— cauen en el mateix resultat, que és just el que demanava l'enunciat.
Exercici 7: SELF JOIN
Dificultat: Intermedi
Enunciat. Per a una campanya de «si t'ha agradat, t'agradarà», necessitem les parelles de materials del mateix autor. Cada parella ha d'aparèixer una sola vegada (no volem veure A-B i també B-A) i cap material no s'ha d'aparellar amb ell mateix. Mostra l'autor i els dos títols.
Pista. La condició m1.material_id < m2.material_id resol els dos problemes alhora.
Solució
SELECT a.nom || ' ' || a.cognoms AS autor,
m1.titol AS titol_1,
m2.titol AS titol_2
FROM materials m1
INNER JOIN materials m2
ON m2.autor_id = m1.autor_id
AND m1.material_id < m2.material_id
INNER JOIN autors a ON a.autor_id = m1.autor_id
ORDER BY a.cognoms, m1.material_id, m2.material_id;Resultat esperat
| autor | titol_1 | titol_2 |
|---|---|---|
| Svetlana Aleksiévitx | Documental: la veu de Txernòbil | Veus de Txernòbil |
| Ken Follett | Els pilars de la Terra | Un món sense fi |
| Ken Follett | Els pilars de la Terra | Els pilars de la Terra (sèrie) |
| Ken Follett | Un món sense fi | Els pilars de la Terra (sèrie) |
| Haruki Murakami | Tokio blues | Kafka a la platja |
Explicació. Un SELF JOIN és un JOIN d'una taula amb ella mateixa; l'únic que el fa possible són els àlies obligatoris (m1, m2), que converteixen una taula en dues «còpies» independents per al motor.
La condició m1.material_id < m2.material_id fa dues feines:
- Amb
<>al seu lloc obtindries 10 files: cada parella duplicada en els dos ordres. - Sense cap condició addicional (només
m2.autor_id = m1.autor_id) obtindries 15 files: les 10 anteriors més les 5 de cada material amb ell mateix.
Fixa't també que els materials 910 i 911 (les revistes, amb autor_id nul) no hi apareixen, i és correcte: NULL = NULL no és cert, així que dues files amb autor desconegut no s'aparellen mai. Aquest comportament, que en altres contextos molesta, aquí és exactament el desitjat: no sabem si són del mateix autor.
Bloc C — Intermedi: agregació i agrupació
Exercici 8: Agregats i GROUP BY d'una i dues columnes, amb HAVING
Dificultat: Intermedi
Enunciat. Tres informes d'inventari i economia:
- (a) Nombre d'exemplars per sucursal, de major a menor.
- (b) Nombre d'exemplars per sucursal i estat.
- (c) Nombre, import total i import mitjà de les multes per motiu, mostrant només els motius amb més d'una multa.
Solució
-- (a) GROUP BY d'una columna
SELECT s.nom AS sucursal, count(*) AS exemplars
FROM exemplars e
JOIN sucursals s ON s.sucursal_id = e.sucursal_id
GROUP BY s.nom
ORDER BY exemplars DESC;
-- (b) GROUP BY de dues columnes
SELECT s.nom AS sucursal, e.estat, count(*) AS n
FROM exemplars e
JOIN sucursals s ON s.sucursal_id = e.sucursal_id
GROUP BY s.nom, e.estat
ORDER BY s.nom, e.estat;
-- (c) Agregats + HAVING
SELECT motiu,
count(*) AS n_multes,
sum(import) AS total,
round(avg(import), 2) AS mitjana
FROM multes
GROUP BY motiu
HAVING count(*) > 1
ORDER BY total DESC;Resultat esperat
(a)
| sucursal | exemplars |
|---|---|
| Centre | 8 |
| Nord | 3 |
| Sud | 2 |
| Est | 2 |
(b)
| sucursal | estat | n |
|---|---|---|
| Centre | disponible | 4 |
| Centre | extraviat | 1 |
| Centre | prestat | 3 |
| Est | reservat | 1 |
| Est | retirat | 1 |
| Nord | disponible | 2 |
| Nord | prestat | 1 |
| Sud | disponible | 2 |
(c)
| motiu | n_multes | total | mitjana |
|---|---|---|---|
| retard | 5 | 15.00 | 3.00 |
| deteriorament | 2 | 10.50 | 5.25 |
Explicació. El punt que cal interioritzar és l'ordre lògic d'execució que vam veure a 02-05: FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.
D'aquí en surten les dues regles pràctiques:
WHEREfiltra files abans d'agrupar;HAVINGfiltra grups després.WHERE count(*) > 1és un error de sintaxi, i no per caprici: quan s'avalua elWHEREels grups encara no existeixen.- Tot el que va al
SELECTi no és dins d'una funció d'agregat ha d'estar alGROUP BY. Si a (a) hi afegississ.sucursal_idalSELECTsense afegir-lo alGROUP BY, PostgreSQL donaria error. (Curiosament sí que ho acceptaria si agrupessis pers.sucursal_id, perquè és clau primària i determina funcionalments.nom: PostgreSQL reconeix aquesta dependència. SQLite i MySQL en mode lax no donen error mai i retornen un valor arbitrari, que és molt pitjor.)
A (c), el motiu perdua desapareix pel HAVING tot i ser la multa més cara (24,00 €). És un recordatori que un HAVING mal triat pot amagar just el que és important.
round(avg(...), 2) necessita que l'argument sigui numeric; com que import és NUMERIC(8,2), funciona. Si la columna fos double precision, PostgreSQL exigiria round(avg(import)::numeric, 2).
Exercici 9: COUNT(*) enfront de COUNT(columna) en un LEFT JOIN
Dificultat: Intermedi
Enunciat. Volem el nombre de préstecs de cada soci, inclosos els socis que no han demanat res mai (han d'aparèixer amb 0). Escriu la consulta i explica per què count(*) donaria un resultat incorrecte.
Pista. Compta el que el LEFT JOIN no ha trobat.
Solució
SELECT so.soci_id,
so.cognoms,
count(p.prestec_id) AS n_prestecs, -- correcte
count(*) AS mal_comptat -- incorrecte, només per veure-ho
FROM socis so
LEFT JOIN prestecs p ON p.soci_id = so.soci_id
GROUP BY so.soci_id, so.cognoms
ORDER BY n_prestecs DESC, so.soci_id;Resultat esperat
| soci_id | cognoms | n_prestecs | mal_comptat |
|---|---|---|---|
| 14 | Alsina | 5 | 5 |
| 15 | Pereda | 4 | 4 |
| 11 | Ferran | 3 | 3 |
| 12 | Rovira | 3 | 3 |
| 16 | Bastos | 3 | 3 |
| 13 | Mestre | 2 | 2 |
| 17 | Vilanova | 0 | 1 |
| 18 | Quintana | 0 | 1 |
Explicació. Aquesta és probablement la trampa més cara del SQL d'informes, perquè no produeix cap error: produeix un número plausible i equivocat.
count(*)compta files. Després delLEFT JOIN, l'Òscar Vilanova genera una fila —la seva, amb totes les columnes deprestecsaNULL—, així quecount(*)retorna 1.count(p.prestec_id)compta valors no nuls d'aquella columna. A la fila de l'Òscar,p.prestec_idésNULL, així que no compta res: 0.
La regla que convé memoritzar: en un LEFT JOIN, compta sempre una columna de la taula dreta, i que aquesta columna sigui la seva clau primària. Si comptessis count(p.data_devolucio), obtindries el nombre de préstecs ja retornats (la Marta Alsina donaria 3 en lloc de 5), que és una altra pregunta perfectament vàlida... però no la que t'han fet.
El mateix s'aplica a sum(): sum(import) sobre un grup sense files retorna NULL, no 0. Si l'informe s'ha de mostrar per pantalla, embolcalla'l: COALESCE(sum(import), 0).
Exercici 10: Rànquing dels materials més prestats
Dificultat: Intermedi
Enunciat. Els cinc materials més prestats de tot l'històric, amb el títol, el nom de l'autor (o (sense autor) si no en té) i el nombre de préstecs. Desempata alfabèticament per títol.
Pista. El préstec apunta a l'exemplar, no al material: cal pujar un esglaó.
Solució
SELECT m.titol,
COALESCE(a.nom || ' ' || a.cognoms, '(sense autor)') AS autor,
count(*) AS n_prestecs
FROM prestecs p
JOIN exemplars e ON e.exemplar_id = p.exemplar_id
JOIN materials m ON m.material_id = e.material_id
LEFT JOIN autors a ON a.autor_id = m.autor_id
GROUP BY m.material_id, m.titol, a.nom, a.cognoms
ORDER BY n_prestecs DESC, m.titol ASC
LIMIT 5;Resultat esperat
| titol | autor | n_prestecs |
|---|---|---|
| El mapa del temps | Félix J. Palma | 5 |
| Els pilars de la Terra | Ken Follett | 3 |
| Kafka a la platja | Haruki Murakami | 3 |
| El cor gelat | Almudena Grandes | 2 |
| Els pilars de la Terra (sèrie) | Ken Follett | 2 |
Explicació. Tres decisions deliberades en aquesta consulta:
- Agrupar per
m.material_id, no perm.titol. Si dos materials diferents compartissin títol —cosa perfectament possible: una edició en paper i una altra en audiollibre— agrupar pel títol els fondria en una sola fila. Agrupar per la clau primària i afegir el títol alGROUP BYper poder projectar-lo és el patró segur. LEFT JOIN autors, noINNER JOIN. Les revistes 910 i 911 tenenautor_idnul. Aquí no es nota perquè no són entre les més prestades, però unINNER JOINles expulsaria silenciosament de qualsevol rànquing futur.COALESCEa la concatenació completa, no a cada tros.a.nom || ' ' || a.cognomsamba.nomnul retornaNULLsencer, perquè qualsevol concatenació ambNULLésNULL. Per això elCOALESCEembolcalla l'expressió ja muntada.
L'empat entre «Els pilars de la Terra» i «Kafka a la platja» (3 préstecs cadascun) es resol pel títol: E abans que K. Sense aquest segon criteri, l'ordre entre tots dos no estaria garantit i el «top 5» podria canviar d'una execució a una altra.
Aquest rànquing és un LIMIT 5 global. La versió «el més prestat de cada sucursal» necessita funcions de finestra i la veuràs a 07-04.
Bloc D — Avançat: subconsultes, taules derivades i CTE
Exercici 11: Subconsulta escalar, IN, EXISTS, NOT EXISTS i correlacionada
Dificultat: Avançat
Enunciat. Cinc preguntes, cadascuna amb l'eina que li correspon:
- (a) Materials anteriors a l'any mitjà del catàleg (subconsulta escalar).
- (b) Socis amb alguna multa pendent, fent servir
IN. - (c) Socis amb algun préstec sense retornar, fent servir
EXISTS. - (d) Sucursals sense cap exemplar extraviat, fent servir
NOT EXISTS. - (e) Per a cada soci, la data del seu últim préstec (subconsulta correlacionada); els socis sense préstecs han de sortir amb
NULL.
Solució
-- (a) Escalar: la subconsulta retorna un únic valor
SELECT titol, any_publicacio
FROM materials
WHERE any_publicacio < (SELECT avg(any_publicacio) FROM materials)
ORDER BY any_publicacio;
-- (b) IN: la subconsulta retorna una llista de valors
SELECT soci_id, nom, cognoms
FROM socis
WHERE soci_id IN (SELECT soci_id FROM multes WHERE estat = 'pendent')
ORDER BY soci_id;
-- (c) EXISTS: la subconsulta només diu sí o no
SELECT so.soci_id, so.nom, so.cognoms
FROM socis so
WHERE EXISTS (SELECT 1 FROM prestecs p
WHERE p.soci_id = so.soci_id AND p.data_devolucio IS NULL)
ORDER BY so.soci_id;
-- (d) NOT EXISTS: antireunió en versió subconsulta
SELECT s.sucursal_id, s.nom
FROM sucursals s
WHERE NOT EXISTS (SELECT 1 FROM exemplars e
WHERE e.sucursal_id = s.sucursal_id AND e.estat = 'extraviat')
ORDER BY s.sucursal_id;
-- (e) Correlacionada al SELECT
SELECT so.soci_id,
so.cognoms,
(SELECT max(p.data_prestec) FROM prestecs p
WHERE p.soci_id = so.soci_id) AS ultim_prestec
FROM socis so
ORDER BY so.soci_id;Resultat esperat
(a) La mitjana del catàleg és 2009,58, així que en surten 6 files: Tokio blues (1987), Els pilars de la Terra (1989), Kafka a la platja (2002), El cor gelat (2007), Un món sense fi (2007), El mapa del temps (2008).
(b) Socis 13 (Mestre), 14 (Alsina) i 15 (Pereda).
(c) Socis 13, 14, 15 i 16.
(d)
| sucursal_id | nom |
|---|---|
| 2 | Nord |
| 3 | Sud |
| 4 | Est |
(e)
| soci_id | cognoms | ultim_prestec |
|---|---|---|
| 11 | Ferran | 2026-07-05 |
| 12 | Rovira | 2026-07-10 |
| 13 | Mestre | 2026-06-15 |
| 14 | Alsina | 2026-07-20 |
| 15 | Pereda | 2026-07-22 |
| 16 | Bastos | 2026-06-10 |
| 17 | Vilanova | (NULL) |
| 18 | Quintana | (NULL) |
Explicació. Cada forma de subconsulta respon a una forma diferent de pregunta:
| Forma | Què retorna la subconsulta | Quan usar-la |
|---|---|---|
| Escalar | Un valor | Comparar cada fila amb un agregat global |
IN (...) |
Una columna de valors | Pertinença a un conjunt conegut |
EXISTS (...) |
Res, només sí/no | «Existeix almenys un», sense importar quants |
NOT EXISTS (...) |
Res, només sí/no | «No n'existeix cap» (antireunió) |
| Correlacionada | Un valor per cada fila externa | Una dada calculada fila a fila |
Dos advertiments que valen el seu pes en hores de depuració:
NOT INi elsNULLno s'avenen. Si escrivissisWHERE sucursal_id NOT IN (SELECT sucursal_id FROM exemplars WHERE estat = 'extraviat')i aquesta subconsulta pogués retornar unNULL, el resultat serien zero files sempre, perquèx NOT IN (1, NULL)s'avalua aNULL.NOT EXISTSno té aquest problema: per això és la forma recomanada per a les antireunions amb subconsulta.SELECT 1dins d'EXISTSno és una superstició ni una optimització: és una declaració d'intencions. AEXISTSli és igual el que hi projectis —el motor ni tan sols ho avalua—, i escriureSELECT 1deixa clar al lector següent que les columnes no importen.
A (e), la correlacionada retorna NULL per als socis sense préstecs perquè max() sobre un conjunt buit és NULL. És exactament el que demanava l'enunciat; si volguessis un text, COALESCE(..., 'mai') requeriria convertir abans la data a text.
Exercici 12: Taula derivada, CTE WITH i UNION
Dificultat: Avançat
Enunciat. Tres consultes que reorganitzen el problema abans de respondre'l:
- (a) El nombre mitjà de préstecs per soci actiu, fent servir una taula derivada.
- (b) Una fitxa per sucursal amb el seu nombre de socis i el seu nombre d'exemplars, fent servir dues CTE.
- (c) Una agenda unificada de contactes: tots els correus de socis actius i de ponents, en una sola llista, amb una columna que indiqui d'on surt cadascun.
Pista. A (b), no intentis fer-ho amb dos JOIN directes sobre sucursals: els recomptes es multiplicarien entre si.
Solució
-- (a) Taula derivada: agreguem i després agreguem sobre el resultat
SELECT round(avg(n), 2) AS mitjana_prestecs_per_soci
FROM (
SELECT so.soci_id, count(p.prestec_id) AS n
FROM socis so
LEFT JOIN prestecs p ON p.soci_id = so.soci_id
WHERE so.actiu
GROUP BY so.soci_id
) AS recomptes;
-- (b) Dues CTE independents, reunides al final
WITH socis_per_sucursal AS (
SELECT sucursal_id, count(*) AS n_socis
FROM socis
GROUP BY sucursal_id
),
exemplars_per_sucursal AS (
SELECT sucursal_id, count(*) AS n_exemplars
FROM exemplars
GROUP BY sucursal_id
)
SELECT s.nom AS sucursal,
COALESCE(sp.n_socis, 0) AS socis,
COALESCE(ep.n_exemplars, 0) AS exemplars
FROM sucursals s
LEFT JOIN socis_per_sucursal sp ON sp.sucursal_id = s.sucursal_id
LEFT JOIN exemplars_per_sucursal ep ON ep.sucursal_id = s.sucursal_id
ORDER BY s.sucursal_id;
-- (c) UNION de dues consultes amb la mateixa forma
SELECT 'soci' AS origen, nom, cognoms, email
FROM socis WHERE actiu
UNION
SELECT 'ponent', nom, cognoms, email
FROM ponents
ORDER BY origen, cognoms;Resultat esperat
(a) 2.86 — hi ha 7 socis actius (tots menys l'Òscar Vilanova) que sumen 20 préstecs: 20 / 7 = 2,857...
(b)
| sucursal | socis | exemplars |
|---|---|---|
| Centre | 3 | 8 |
| Nord | 2 | 3 |
| Sud | 2 | 2 |
| Est | 1 | 2 |
(c) 10 files: 3 ponents (Calduch, Lemus, Marchetti) i 7 socis actius.
Explicació. Les tres són variants del mateix moviment: calcular un resultat intermedi i consultar-lo com si fos una taula.
A (a), l'agregació doble és obligatòria. avg(count(*)) no existeix: no es poden imbricar funcions d'agregat. Cal agregar una vegada (préstecs per soci), materialitzar aquell resultat com a taula derivada i agregar una altra vegada. Fixa't en l'AS recomptes: a PostgreSQL una taula derivada necessita àlies o el motor dóna error, encara que no el facis servir.
A (b) hi ha la lliçó important. Si escrivissis això:
-- MALAMENT: els recomptes es multipliquen
SELECT s.nom, count(DISTINCT so.soci_id), count(DISTINCT e.exemplar_id)
FROM sucursals s
LEFT JOIN socis so ON so.sucursal_id = s.sucursal_id
LEFT JOIN exemplars e ON e.sucursal_id = s.sucursal_id
GROUP BY s.nom;...el count(DISTINCT ...) et salvaria de miracle, però un count(*) donaria 3 × 8 = 24 per a Centre. El JOIN de dues branques independents contra la mateixa taula produeix un producte cartesià dins de cada sucursal. Aquest és l'error d'agregació més car que existeix perquè el resultat sembla raonable. La solució robusta és agregar cada branca per separat —en CTE o en subconsultes— i reunir després.
A (c), UNION elimina duplicats i UNION ALL no. Aquí no hi ha duplicats possibles perquè la primera columna ja els distingeix, així que UNION ALL seria més ràpid. Les dues branques han de tenir el mateix nombre de columnes i tipus compatibles, i els noms els posa sempre la primera branca: per això els àlies només calen a dalt. L'ORDER BY va una sola vegada, al final, i afecta el conjunt unit.
Bloc E — Avançat: informes de gestió reals
Exercici 13: Recaptació de multes per mes i mètode de pagament
Dificultat: Avançat
Enunciat. Intervenció municipal demana la recaptació de multes: per mes i mètode de pagament, el nombre de pagaments i l'import total, més una columna canal que valgui 'presencial' per a efectiu i targeta i 'en línia' per a passarel·la. Afegeix-hi una última consulta amb el total pendent de cobrament (multes en estat pendent).
Pista. to_char(data_pagament, 'YYYY-MM') agrupa per mes a PostgreSQL.
Solució
-- Recaptació per mes i mètode
SELECT to_char(pg.data_pagament, 'YYYY-MM') AS mes,
pg.metode,
CASE WHEN pg.metode IN ('efectiu', 'targeta') THEN 'presencial'
WHEN pg.metode = 'passarella' THEN 'en línia'
ELSE 'desconegut'
END AS canal,
count(*) AS n_pagaments,
sum(pg.import) AS recaptat
FROM pagaments pg
GROUP BY 1, 2, 3
ORDER BY mes, pg.metode;
-- Pendent de cobrament
SELECT count(*) AS multes_pendents, sum(import) AS import_pendent
FROM multes
WHERE estat = 'pendent';Resultat esperat
| mes | metode | canal | n_pagaments | recaptat |
|---|---|---|---|---|
| 2026-03 | efectiu | presencial | 1 | 2.20 |
| 2026-04 | targeta | presencial | 1 | 3.00 |
| 2026-05 | passarella | en línia | 1 | 2.50 |
| 2026-05 | targeta | presencial | 1 | 4.00 |
Total recaptat: 11,70 €. Pendent: 3 multes per 30,60 €.
Explicació. L'informe té tres punts fins:
- S'agrupa per
pagaments, no permultes. Una multa es pot cobrar en diversos terminis: la multa 5 (6,50 €, deteriorament, Núria Bastos) es va cobrar en dos pagaments, 4,00 € amb targeta al maig i 2,50 € per passarel·la també al maig. Si l'informe es construís sobremultes, aquest cas es comptaria malament així que els dos terminis caiguessin en mesos diferents. La pregunta «quant va entrar a caixa al març» es respon sempre sobre la taula de moviments, no sobre la de deutes. GROUP BY 1, 2, 3agrupa per posició alSELECT. És còmode, però fràgil: si hi insereixes una columna al principi, l'agrupació canvia sense avisar. En consultes que han de durar, escriu-hi els noms o repeteix l'expressióCASEcompleta alGROUP BY.- Les multes
condonadaianulladano apareixen enlloc de l'informe, i és correcte: no s'han cobrat ni es cobraran. La multa 6 (4,00 €, condonada) i la 8 (3,20 €, anul·lada) sumen 7,20 € que no són ni ingrés ni deute. Un informe que les barregés amb les pendents inflaria la previsió de cobrament en un 23 %.
A SQLite, to_char no existeix: es fa servir strftime('%Y-%m', data_pagament).
Exercici 14: Préstecs vençuts amb el recàrrec calculat
Dificultat: Avançat
Enunciat. A data 2 d'agost de 2026, llista els préstecs vençuts i sense retornar: soci, codi i títol, data prevista, dies de retard i el recàrrec a raó de 0,20 €/dia amb un topall de 15,00 €. Ordena per recàrrec descendent.
Pista. La resta de dues DATE a PostgreSQL retorna un enter de dies. El topall s'aplica amb LEAST.
Solució
SELECT so.nom || ' ' || so.cognoms AS soci,
e.codi,
m.titol,
p.data_devolucio_prevista AS prevista,
(DATE '2026-08-02' - p.data_devolucio_prevista) AS dies_retard,
LEAST((DATE '2026-08-02' - p.data_devolucio_prevista) * 0.20, 15.00) AS recarrec
FROM prestecs p
JOIN socis so ON so.soci_id = p.soci_id
JOIN exemplars e ON e.exemplar_id = p.exemplar_id
JOIN materials m ON m.material_id = e.material_id
WHERE p.data_devolucio IS NULL
AND p.data_devolucio_prevista < DATE '2026-08-02'
ORDER BY recarrec DESC, dies_retard DESC;Resultat esperat
| soci | codi | titol | prevista | dies_retard | recarrec |
|---|---|---|---|---|---|
| Sònia Mestre | EJ-3088 | Tokio blues | 2026-04-26 | 98 | 15.00 |
| Marta Alsina | EJ-3093 | Veus de Txernòbil | 2026-05-31 | 63 | 12.60 |
| Núria Bastos | EJ-3087 | El cor gelat | 2026-07-01 | 32 | 6.40 |
Explicació. Tres coses que cal mirar amb lupa:
- El
WHEREporta dues condicions i totes dues són imprescindibles.data_devolucio IS NULLselecciona els préstecs oberts;data_devolucio_prevista < '2026-08-02'selecciona els vençuts. Amb només la primera, els préstecs 17 i 19 (previstos per al 10 i el 12 d'agost) apareixerien amb dies de retard negatius i un recàrrec negatiu: la biblioteca devent diners al soci. - El topall s'aplica i es nota. Sense
LEAST, la Sònia Mestre pagaria 19,60 € (98 × 0,20). El reglament fixa 15,00 €, iLEAST(a, b)retorna el menor dels dos. A MySQL la funció també es diuLEAST; a SQLite ésMIN(a, b)amb dos arguments, que no és la funció d'agregatMIN()tot i el nom. - L'aritmètica de dates és específica de cada motor. A PostgreSQL,
date - dateretornainteger(dies). A SQLite cal escriurejulianday('2026-08-02') - julianday(data_devolucio_prevista), i a MySQLDATEDIFF('2026-08-02', data_devolucio_prevista). És dels primers llocs on una consulta portable deixa de ser-ho.
Nota de coherència: l'exemplar EJ-3093 figura com a extraviat i el seu préstec segueix obert. Ja té una multa per pèrdua (24,00 €), així que en el procés real caldria excloure'l dels recàrrecs per retard. Afegir AND e.estat <> 'extraviat' és una decisió de negoci perfectament defensable; l'enunciat no la demanava, però un informe que es lliura a direcció l'hauria de documentar.
Exercici 15: Ocupació d'esdeveniments per sucursal
Dificultat: Avançat
Enunciat. Per als esdeveniments ja celebrats, calcula per sucursal: nombre d'esdeveniments, places ofertes, places ocupades i percentatge d'ocupació amb un decimal. Només compten com a ocupades les inscripcions en estat confirmada o assistida (les cancel·lades i les de llista d'espera, no).
Pista. Si uneixes esdeveniments amb inscripcions i sumes places_ofertes, el resultat estarà inflat. Agrega primer per esdeveniment.
Solució
WITH ocupacio_esdeveniment AS (
SELECT ev.esdeveniment_id,
ev.sala_id,
ev.places_ofertes,
COALESCE(sum(i.places_ocupades) FILTER (
WHERE i.estat IN ('confirmada', 'assistida')), 0) AS ocupades
FROM esdeveniments ev
LEFT JOIN inscripcions i ON i.esdeveniment_id = ev.esdeveniment_id
WHERE ev.estat = 'celebrat'
GROUP BY ev.esdeveniment_id, ev.sala_id, ev.places_ofertes
)
SELECT su.nom AS sucursal,
count(*) AS esdeveniments,
sum(oe.places_ofertes) AS ofertes,
sum(oe.ocupades) AS ocupades,
round(100.0 * sum(oe.ocupades) / sum(oe.places_ofertes), 1) AS pct_ocupacio
FROM ocupacio_esdeveniment oe
JOIN sales sa ON sa.sala_id = oe.sala_id
JOIN sucursals su ON su.sucursal_id = sa.sucursal_id
GROUP BY su.sucursal_id, su.nom
ORDER BY pct_ocupacio DESC;Resultat esperat
| sucursal | esdeveniments | ofertes | ocupades | pct_ocupacio |
|---|---|---|---|---|
| Sud | 1 | 8 | 5 | 62.5 |
| Nord | 1 | 20 | 9 | 45.0 |
| Est | 1 | 10 | 4 | 40.0 |
| Centre | 2 | 52 | 15 | 28.8 |
Explicació. Aquest exercici reuneix gairebé tot el bloc en una sola consulta, i el seu valor és a la CTE.
Per què la CTE és obligatòria. Si sumessis places_ofertes directament sobre el JOIN d'esdeveniments amb inscripcions, cada esdeveniment apareixeria tantes vegades com inscripcions tingui. L'esdeveniment 101 té 5 inscripcions, així que les seves 12 places ofertes es comptarien 5 vegades: 60. Centre passaria de 52 a 100 places ofertes i l'ocupació cauria del 28,8 % al 15 %. L'error no dóna cap símptoma: els números segueixen essent enters raonables. La regla general és: quan un informe suma un atribut de l'«un» i un altre atribut del «molts», el de l'«un» cal agregar-lo a part.
El filtre per estat va al FILTER, no al WHERE. sum(...) FILTER (WHERE ...) aplica la condició només a l'agregat, deixant intactes les altres files del grup. Si posessis i.estat IN ('confirmada','assistida') al WHERE de la CTE, un esdeveniment les inscripcions del qual fossin totes cancel·lades desapareixeria de l'informe en lloc d'aparèixer amb 0 ocupades. FILTER és SQL estàndard i PostgreSQL l'admet; l'equivalent portable és sum(CASE WHEN i.estat IN ('confirmada','assistida') THEN i.places_ocupades ELSE 0 END), que funciona també a SQLite i MySQL.
100.0 i no 100. Si escrius 100 * sum(ocupades) / sum(ofertes) amb enters, PostgreSQL fa divisió entera i Centre donaria 28 en lloc de 28.8, i un cas com 5/8 donaria 62 en comptes de 62.5. Multiplicar primer per 100.0 força l'aritmètica decimal. Aquest error apareix en producció constantment.
El LEFT JOIN continua essent necessari encara que en aquestes dades tots els esdeveniments celebrats tinguin inscripcions: si demà se'n celebra un al qual no s'apunta ningú, amb INNER JOIN desapareixeria del denominador i l'ocupació mitjana sortiria artificialment alta.
Errors Habituals i Consells
1. = NULL en lloc d'IS NULL. No dóna error: dóna zero files. Cada vegada que una consulta et retorni un conjunt buit inesperat, aquesta és la primera sospita.
2. count(*) en un LEFT JOIN. Compta 1 on hauria de comptar 0. Compta sempre la clau primària de la taula dreta.
3. Sumar atributs de l'«un» sobre un JOIN amb el «molts». Els totals es multipliquen pel nombre de files filles. Agrega cada branca a la seva pròpia CTE o subconsulta.
4. HAVING i WHERE intercanviats. WHERE filtra files abans d'agrupar, HAVING filtra grups després. Posar al HAVING una condició que podria anar al WHERE funciona però és més lent: obliga a agrupar files que es descartaran.
5. ORDER BY sense desempat. Un LIMIT sobre un ordre no determinista retorna files diferents en execucions diferents. Afegeix-hi sempre un criteri únic com a últim desempat.
6. Divisió entera. 100 * a / b amb enters trunca. Força els decimals amb 100.0 o amb ::numeric.
7. NOT IN amb subconsultes que poden retornar NULL. Retorna zero files sempre. Fes servir NOT EXISTS.
8. Confondre la sucursal del soci amb la sucursal de l'exemplar. Són dues claus foranes diferents cap a la mateixa taula. Quan la pregunta diu «per sucursal», esbrina quina sucursal abans d'escriure el JOIN.
9. Oblidar l'ON d'un JOIN. Producte cartesià silenciós. Amb taules petites sembla que funciona; amb 40.000 exemplars no.
10. Concatenar amb NULL. 'a' || NULL és NULL. Embolcalla l'expressió completa amb COALESCE, no cada tros.
Consell de mètode. Construeix les consultes grans de dins cap enfora: escriu primer el FROM amb els seus JOIN i un SELECT *, comprova el nombre de files, afegeix-hi el WHERE, torna a comprovar, i només al final agrupa i projecta. El 90 % dels errors d'agregació es detecten mirant quantes files hi ha abans d'agrupar.
Consell de lectura. Quan heretis una consulta aliena, llegeix-la en l'ordre lògic d'execució (FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY), no en l'ordre en què està escrita. És l'ordre en què el motor l'entén.
Exercicis
Sense pistes i més exigents. Escriu cadascun com una sola consulta.
Exercici A: Fitxa econòmica d'un soci
Per a la Marta Alsina (sòcia 14), retorna una única fila amb: cognoms, nombre total de préstecs, nombre de préstecs retornats amb retard, nombre de préstecs oberts, import total de les seves multes, import efectivament pagat i import pendent.
Exercici B: Materials presents en tres o més sucursals
Llista els materials que tenen exemplars en tres o més sucursals diferents, amb el títol, el nombre de sucursals i el nombre total d'exemplars. Un material amb cinc exemplars a la mateixa sucursal no compta.
Exercici C: Socis en situació de risc
Retorna una llista de socis en risc amb una columna motiu. Un soci està en risc si té algun préstec vençut sense retornar a 2026-08-02 (motiu 'préstec vençut') o si acumula més de 10 € en multes pendents (motiu 'deute alt'). Un soci pot aparèixer pels dos motius.
Solucions
Solució A
SELECT so.cognoms,
count(p.prestec_id) AS n_prestecs,
count(*) FILTER (WHERE p.data_devolucio > p.data_devolucio_prevista) AS amb_retard,
count(*) FILTER (WHERE p.data_devolucio IS NULL) AS oberts,
(SELECT COALESCE(sum(mu.import), 0) FROM multes mu
WHERE mu.soci_id = so.soci_id) AS multes_total,
(SELECT COALESCE(sum(pg.import), 0) FROM pagaments pg
JOIN multes mu ON mu.multa_id = pg.multa_id
WHERE mu.soci_id = so.soci_id) AS pagat,
(SELECT COALESCE(sum(mu.import), 0) FROM multes mu
WHERE mu.soci_id = so.soci_id AND mu.estat = 'pendent') AS pendent
FROM socis so
LEFT JOIN prestecs p ON p.soci_id = so.soci_id
WHERE so.soci_id = 14
GROUP BY so.soci_id, so.cognoms;| cognoms | n_prestecs | amb_retard | oberts | multes_total | pagat | pendent |
|---|---|---|---|---|---|---|
| Alsina | 5 | 1 | 2 | 26.20 | 2.20 | 24.00 |
Les tres xifres econòmiques van en subconsultes escalars precisament per evitar el problema de l'exercici 15: si unissis multes i pagaments al mateix JOIN que prestecs, cada multa es repetiria cinc vegades (una per préstec) i el total saltaria de 26,20 € a 131,00 €. Els FILTER sobre count(*) funcionen aquí perquè el LEFT JOIN amb prestecs no duplica res: cada préstec és una fila.
Solució B
SELECT m.material_id,
m.titol,
count(DISTINCT e.sucursal_id) AS sucursals,
count(*) AS exemplars
FROM materials m
JOIN exemplars e ON e.material_id = m.material_id
GROUP BY m.material_id, m.titol
HAVING count(DISTINCT e.sucursal_id) >= 3
ORDER BY sucursals DESC, m.titol;| material_id | titol | sucursals | exemplars |
|---|---|---|---|
| 901 | Els pilars de la Terra | 3 | 3 |
Només «Els pilars de la Terra» està repartit en tres sucursals (Centre, Sud i Est). El DISTINCT dins del count és el que distingeix «tres sucursals» de «tres exemplars»: sense ell, el material 906 (dos exemplars, a Sud i Centre) seguiria sense colar-se, però un material amb tres exemplars a Centre sí que ho faria, i seria un error.
Solució C
SELECT so.soci_id, so.nom, so.cognoms, 'préstec vençut' AS motiu
FROM socis so
WHERE EXISTS (SELECT 1 FROM prestecs p
WHERE p.soci_id = so.soci_id
AND p.data_devolucio IS NULL
AND p.data_devolucio_prevista < DATE '2026-08-02')
UNION ALL
SELECT so.soci_id, so.nom, so.cognoms, 'deute alt'
FROM socis so
JOIN multes mu ON mu.soci_id = so.soci_id AND mu.estat = 'pendent'
GROUP BY so.soci_id, so.nom, so.cognoms
HAVING sum(mu.import) > 10
ORDER BY 1, 4;| soci_id | nom | cognoms | motiu |
|---|---|---|---|
| 13 | Sònia | Mestre | préstec vençut |
| 14 | Marta | Alsina | deute alt |
| 14 | Marta | Alsina | préstec vençut |
| 16 | Núria | Bastos | préstec vençut |
La Marta Alsina apareix dues vegades, que és just el que demanava l'enunciat; per això cal fer servir UNION ALL i no UNION (encara que aquí UNION donaria el mateix, perquè la columna motiu ja diferencia les files). Fixa't que l'Ivan Pereda no hi surt: té una multa pendent, però d'1,60 €, i el seu préstec obert venç el 12 d'agost. I la Sònia Mestre hi surt només pel préstec, perquè els seus 5,00 € pendents no arriben al llindar.
Conclusió
Has escrit quinze consultes sobre BiblioRed que recorren tot el SQL del mòdul 2, aquesta vegada sense xarxa: projecció amb àlies, filtres amb BETWEEN, IN, LIKE i IS NULL, ordenació determinista amb LIMIT, DML amb la disciplina del SELECT previ, INNER JOIN de dues i de quatre taules, LEFT JOIN amb el patró d'antireunió, SELF JOIN, agregació amb GROUP BY d'una i dues columnes, HAVING, la diferència entre COUNT(*) i COUNT(columna), les cinc formes de subconsulta, taules derivades, CTE, UNION i tres informes de gestió que ja s'assemblen al que demana una direcció de servei.
Més important que la sintaxi són els tres reflexos que se t'haurien d'haver instal·lat: mirar abans de modificar, comprovar el nombre de files abans d'agregar i desconfiar de tot informe que suma un atribut del costat «un» a través d'un JOIN amb el costat «molts». Cap dels tres no l'ensenya a ningú un missatge d'error, perquè tots tres fallen en silenci.
La lliçó següent canvia de múscul. A 07-02, Exercicis de Disseny d'Esquemes, no hi haurà taules per consultar: hi haurà enunciats de requisits —un videoclub, una plataforma de cursos, una clínica, un sistema de tarifes històriques— i hauràs de construir l'esquema des de zero, passant pel diagrama ER, el CREATE TABLE i les restriccions que codifiquen les regles de negoci. Un dels cinc casos serà una ampliació de BiblioRed, perquè comprovis com n'és, de diferent, dissenyar sobre un esquema que ja existeix i que no pots trencar.
Fonaments de Bases de Dades
Mòdul 1: Introducció a les Bases de Dades
- Conceptes Bàsics de Bases de Dades
- Tipus de Bases de Dades
- Història i Evolució de les Bases de Dades
- Sistemes Gestors de Bases de Dades i Arquitectura
Mòdul 2: Bases de Dades Relacionals
- Model Relacional
- Llenguatge SQL
- Operacions Bàsiques en SQL
- Consultes Multitaula: JOIN i Subconsultes
- Agregació i Agrupació de Dades
- Integritat Referencial
Mòdul 3: Bases de Dades No Relacionals
- Introducció a NoSQL
- Tipus de Bases de Dades NoSQL
- Modelatge de Dades en NoSQL
- Comparació entre Bases de Dades Relacionals i No Relacionals
Mòdul 4: Disseny d'Esquemes
- Principis de Disseny d'Esquemes
- Diagrames Entitat-Relació (ER)
- Transformació de Diagrames ER a Esquemes Relacionals
- Tipus de Dades i Restriccions
Mòdul 5: Normalització
Mòdul 6: Transaccions, Rendiment i Seguretat
- Transaccions i Propietats ACID
- Concurrència i Nivells d'Aïllament
- Índexs i Optimització de Consultes
- Seguretat, Permisos i Còpies de Seguretat
Mòdul 7: Exercicis Pràctics
- Exercicis de SQL
- Exercicis de Disseny d'Esquemes
- Exercicis de Normalització
- Exercicis de Consultes Avançades i Transaccions
Mòdul 8: Casos d'Estudi
- Cas d'Estudi: Base de Dades Relacional
- Cas d'Estudi: Base de Dades No Relacional
- Cas d'Estudi: Persistència Poliglota
