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:

createdb biblioredx
psql -d biblioredx -f dades_m7.sql
-- ============================================================
-- 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

  1. Bloc A — Bàsic: consultes sobre una sola taula (exercicis 1-3)
  2. Bloc B — Intermedi: consultes sobre diverses taules (exercicis 4-7)
  3. Bloc C — Intermedi: agregació i agrupació (exercicis 8-10)
  4. Bloc D — Avançat: subconsultes, taules derivades i CTE (exercicis 11-12)
  5. Bloc E — Avançat: informes de gestió reals (exercicis 13-15)
  6. Errors habituals i consells
  7. 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ítol fallaria o es convertiria en títol en 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. Un ORDER BY que no desempata no és determinista, i en un llistat amb LIMIT això significa que la fila que veus pot variar entre execucions.
  • LIMIT va 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 prestat o reservat, 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_url no 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 2010 inclou els dos extrems. Si necessites excloure'ls, BETWEEN no serveix: cal escriure > 2000 AND < 2010. I l'ordre importa: BETWEEN 2010 AND 2000 retorna zero files, sense error i sense avís.
  • IN ('prestat','reservat') és exactament equivalent a estat = 'prestat' OR estat = 'reservat'. És més llegible i, sobretot, evita l'error de precedència d'escriure WHERE material_id = 901 AND estat = 'prestat' OR estat = 'reservat', que no significa el que sembla: AND lliga més fort que OR.
  • ILIKE és una extensió de PostgreSQL. A SQLite, LIKE ja é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 és WHERE lower(titol) LIKE lower('%txernòbil%').
  • portada_url = NULL retorna NULL, que en un WHERE es 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, i count(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 2

Explicació. Tres hàbits que eviten gairebé tots els desastres d'un taulell:

  1. Llista de columnes explícita a l'INSERT. INSERT INTO socis VALUES (...) funciona fins al dia que algú afegeix una columna a socis; llavors tots els INSERT sense llista es trenquen o, pitjor, col·loquen els valors a la columna equivocada.
  2. El SELECT amb el mateix WHERE, primer. No és una recomanació d'estil: és l'única manera de saber quantes files tocaràs abans de tocar-les. Un UPDATE socis SET sucursal_id = 2 sense WHERE afecta els vuit socis i no hi ha «desfer» fora d'una transacció.
  3. RETURNING (PostgreSQL) retorna les files realment afectades. És la confirmació posterior que el WHERE va 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 columna obert; el fet de no tenir data de devolució és el fet d'estar obert. És un ús de NULL legítim: significa «encara no ha passat».
  • La sucursal ve d'exemplars, no de socis. 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.nom diria 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:

  1. LEFT JOIN, no INNER JOIN. L'INNER descarta precisament les files que busquem.
  2. La condició de l'aparellament va a l'ON.
  3. El filtre IS NULL va al WHERE i ha d'apuntar a una columna de la taula dreta que mai no sigui nul·la per si mateixa: per això fem servir p.prestec_id (clau primària, mai nul·la) i no p.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: FROMJOINWHEREGROUP BYHAVINGSELECTORDER BY.

D'aquí en surten les dues regles pràctiques:

  • WHERE filtra files abans d'agrupar; HAVING filtra grups després. WHERE count(*) > 1 és un error de sintaxi, i no per caprici: quan s'avalua el WHERE els grups encara no existeixen.
  • Tot el que va al SELECT i no és dins d'una funció d'agregat ha d'estar al GROUP BY. Si a (a) hi afegissis s.sucursal_id al SELECT sense afegir-lo al GROUP BY, PostgreSQL donaria error. (Curiosament sí que ho acceptaria si agrupessis per s.sucursal_id, perquè és clau primària i determina funcionalment s.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 del LEFT JOIN, l'Òscar Vilanova genera una fila —la seva, amb totes les columnes de prestecs a NULL—, així que count(*) retorna 1.
  • count(p.prestec_id) compta valors no nuls d'aquella columna. A la fila de l'Òscar, p.prestec_id és NULL, 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 per m.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 al GROUP BY per poder projectar-lo és el patró segur.
  • LEFT JOIN autors, no INNER JOIN. Les revistes 910 i 911 tenen autor_id nul. Aquí no es nota perquè no són entre les més prestades, però un INNER JOIN les expulsaria silenciosament de qualsevol rànquing futur.
  • COALESCE a la concatenació completa, no a cada tros. a.nom || ' ' || a.cognoms amb a.nom nul retorna NULL sencer, perquè qualsevol concatenació amb NULL és NULL. Per això el COALESCE embolcalla 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ó:

  1. NOT IN i els NULL no s'avenen. Si escrivissis WHERE sucursal_id NOT IN (SELECT sucursal_id FROM exemplars WHERE estat = 'extraviat') i aquesta subconsulta pogués retornar un NULL, el resultat serien zero files sempre, perquè x NOT IN (1, NULL) s'avalua a NULL. NOT EXISTS no té aquest problema: per això és la forma recomanada per a les antireunions amb subconsulta.
  2. SELECT 1 dins d'EXISTS no és una superstició ni una optimització: és una declaració d'intencions. A EXISTS li és igual el que hi projectis —el motor ni tan sols ho avalua—, i escriure SELECT 1 deixa 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 per multes. 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 sobre multes, 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, 3 agrupa per posició al SELECT. É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ó CASE completa al GROUP BY.
  • Les multes condonada i anullada no 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 WHERE porta dues condicions i totes dues són imprescindibles. data_devolucio IS NULL selecciona 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 €, i LEAST(a, b) retorna el menor dels dos. A MySQL la funció també es diu LEAST; a SQLite és MIN(a, b) amb dos arguments, que no és la funció d'agregat MIN() tot i el nom.
  • L'aritmètica de dates és específica de cada motor. A PostgreSQL, date - date retorna integer (dies). A SQLite cal escriure julianday('2026-08-02') - julianday(data_devolucio_prevista), i a MySQL DATEDIFF('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.

© Copyright 2026. Tots els drets reservats