Esercizio - Interrogazioni sullo stabilimento balneare (tema d'esame 18 giugno 2012)
In questa pagina 6
Testo (compito d'esame del 18 giugno 2012, "Stabilimento balneare", dal corso di Basi di Dati, Ing. Informatica UniPD). Lo stabilimento "Bella Napoli" vuole informatizzare la prenotazione dei posti ombrellone, a partire dalle stagioni estive dell'anno corrente:
- ogni ombrellone è identificato da un numero (anche zero) e ha zero, uno o due lettini (che hanno un numero identificativo); lo stabilimento dispone, ogni stagione, di un certo numero di posti ombrellone che può variare da una stagione all'altra;
- chi vuole un ombrellone deve lasciare gli estremi di un documento (basta uno in caso di due persone per lo stesso ombrellone); il prezzo di un ombrellone è di 5 euro all'ora; si possono prendere in prestito teli da mare a 3 euro a telo (i prezzi possono cambiare da una stagione all'altra); i teli da mare non sono molti, sicuramente meno dei lettini; ogni stagione ha un numero massimo di teli;
- per ogni persona che prende un ombrellone si registra l'orario di entrata e quello di uscita; si può prenotare lo stesso ombrellone più volte nell'arco di una giornata.
Interrogazioni da scrivere in algebra relazionale e in SQL:
- calcolare quanti lettini sono stati utilizzati il 18 giugno 2012;
- controllare che, ad una certa ora del giorno, il numero di teli da mare prestati non sia superiore al massimo a disposizione;
- calcolare l'età media delle persone che hanno prenotato un ombrellone.
Qui si riporta lo schema relazionale della soluzione del corso e si risolvono le tre query; la progettazione ER (ombrellone identificato esternamente dalla stagione, prenotazione, lettini) è nell'esercizio di progettazione della stessa traccia. Teoria: SQL - interrogazioni, join e aggregazioniUn'interrogazione SQL ha almeno SELECT e FROM; le altre clausole (WHERE, GROUP BY, HAVING, ORDER BY) sono opzionali e HAVING richiede GROUP BY. Ordine logico di valutazione: FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY. SELECT corrisponde a π (ma senza DISTINCT non elimina i duplicati), WHERE a σ, gli alias ad AS/ρ, i join a ⋈ (INNER, LEFT, RIGHT, FULL). WHERE seleziona le righe prima del raggruppamento, HAVING seleziona i gruppi dopo. Le funzioni COUNT, SUM, AVG, MIN, MAX ignorano i NULL (COUNT() conta le righe, DISTINCT toglie i duplicati dentro la funzione); con un aggregato o con GROUP BY nella SELECT possono comparire solo colonne di raggruppamento o aggregati. LIKE usa % (qualsiasi sequenza) e _ (un carattere). I confronti con NULL non sono veri: serve IS NULL. Un filtro sulla tabella di destra di un LEFT JOIN va nella clausola ON, non in WHERE, altrimenti il join diventa interno. Con interi COUNT(a)/COUNT(b) è una divisione intera: serve CAST.SQL - interrogazioni, join e aggregazioni →, Algebra relazionale estesa - divisione, aggregazione e outer joinL'algebra relazionale di base lavora una tupla alla volta e non sa fare calcoli sui valori né riassumere più tuple. L'estensione aggiunge: la proiezione generalizzata π_{F1,...,Fn}(R), che calcola espressioni sugli attributi (prezzo per quantità); l'aggregazione ℱ_{SUM, MAX, MIN, AVG, COUNT}(R), che produce una sola tupla; il raggruppamento (A1,...,Am)ℱ_{f(B)}(R), che produce una tupla per gruppo (grado n+m, cardinalità = numero di gruppi). Le funzioni di aggregazione ignorano i NULL, COUNT() conta le tuple, i NULL formano un gruppo a sé. Gli outer join (left, right, full) mantengono con NULL le tuple dangling del join; i NULL nella chiave di join non si accoppiano mai. La divisione R÷S dà gli x tali che (x,y) sta in R per ogni y di S, e si ottiene con π, × e −. Ricette: massimo = aggregazione + join con il massimo, "tutti" = divisione o complemento, "almeno due" = self-join.Algebra relazionale estesa - divisione, aggregazione e outer join →, SQL avanzato - indici, transazioni e triggerUn file di record può essere heap (inserimento in ordine, ricerca per uguaglianza in media n/2 accessi), ordinato (log2 n), hash (costante, solo uguaglianza) o B-tree (log_F n, anche intervalli). Un indice è una struttura ausiliaria di coppie (chiave K, puntatore P) ordinate, molto più piccola della tabella: l'indice primario è sul campo di ordinamento del file ordinato (una chiave per blocco, non denso, log2(m)+1 accessi), l'indice secondario è su un campo qualsiasi (denso). CREATE [UNIQUE] INDEX, anche su espressioni; PostgreSQL usa tabelle heap con indici secondari B-tree (predefinito) o hash, e PRIMARY KEY e UNIQUE creano un indice implicito. Gli indici velocizzano le letture ma costano nelle scritture e in spazio. Una transazione è una sequenza atomica di operazioni con proprietà ACID (atomicità, consistenza, isolamento, durabilità): START TRANSACTION, COMMIT, SAVEPOINT, ROLLBACK [TO SAVEPOINT]. Un trigger è una regola evento-condizione-azione (ECA): BEFORE/AFTER, ROW/STATEMENT, WHEN, con funzione PL/pgSQL che usa NEW e OLD; serve per vincoli complessi e attributi derivati.SQL avanzato - indici, transazioni e trigger →.
Progettazione ER e schema relazionale della stessa traccia: Esercizio - Progettazione ER e schema relazionale dello stabilimento balneare (tema d'esame 18 giugno 2012).
Schema relazionale (dalla soluzione del corso)
Chiavi in grassetto.
| Relazione | Attributi | Note |
|---|---|---|
STAGIONE |
anno, n_teli, prezzo_ombrellone, prezzo_telo | i prezzi e il massimo di teli sono per stagione |
OMBRELLONE |
numero, stagione STAGIONE, prenotato |
identificazione esterna: il numero vale dentro la stagione; prenotato diventa vero all'ingresso e falso all'uscita |
CLIENTE |
documento, data_nascita | |
PRENOTAZIONE |
numero, n_teli, ingresso, uscita, OMBRELLONE, cliente CLIENTE |
uscita è NULL finché la persona è presente; ingresso e uscita sono data e ora |
LETTINO |
numero | |
COLLOCATO |
prenotazione PRENOTAZIONE, lettino LETTINO |
un lettino è associato a una prenotazione solo se non c'è conflitto temporale |
Vincoli esterni: ogni prenotazione ha al più 2 lettini; un lettino non può essere associato a due prenotazioni sovrapposte nel tempo; il numero di teli in uso non supera STAGIONE.n_teli.
Dati inventati. Stagione 2012: 5 teli. Ombrelloni 1-4. Clienti: A (nato 1990-05-10), B (1985-12-01), C (2000-02-29), E (1970-01-01, mai prenotato). Prenotazioni (numero; cliente; ombrellone; ingresso; uscita; teli): 1; A; 1; 2012-06-18 09:00; 13:00; 2 — 2; B; 1; 2012-06-18 14:00; (presente); 1 — 3; C; 2; 2012-06-18 10:00; (presente); 2 — 4; A; 3; 2012-06-17 10:00; 18:00; 1. COLLOCATO: (1,1), (1,2), (2,1), (3,3), (3,4), (4,5). L'ombrellone 1 è prenotato due volte nella stessa giornata (prenotazioni 1 e 2) e il lettino 1 è riusato.
Query 1: quanti lettini sono stati utilizzati il 18 giugno 2012
Idea. Le prenotazioni di quel giorno, collegate ai lettini tramite COLLOCATO, e un conteggio.
Soluzione del corso (algebra).
Due imprecisioni.
ingressoè un timestamp (data e ora):ingresso = '2012-06-18'confronta un'ora precisa con una data e non trova nulla (provato: 0 righe). Si usa la parte data:DATE(ingresso) = '2012-06-18'(oppure un intervallo giorno e giorno dopo, che permette di usare un indice).COUNT(*)conta le coppie (prenotazione, lettino), non i lettini: il lettino 1, usato nella prenotazione 1 al mattino e nella 2 nel pomeriggio, è contato due volte. "Quanti lettini sono stati utilizzati" chiede i lettini distinti.
SQL.
SELECT COUNT(*) AS assegnazioni,
COUNT(DISTINCT co.lettino) AS lettini_usati
FROM collocato AS co INNER JOIN prenotazione AS p ON co.prenotazione = p.numero
WHERE DATE(p.ingresso) = '2012-06-18';(DATE() funziona in SQLite e in PostgreSQL con una lieve variante, p.ingresso::date.) Risultato: 5 assegnazioni, 4 lettini distinti (1, 2, 3, 4). Il lettino 5 è della prenotazione del 17 giugno.
Algebra con i distinti. L'algebra non ha COUNT(DISTINCT ...): si proietta prima, e la proiezione elimina i duplicati.
(Provato con un interprete di algebra: 5 senza la proiezione, 4 con.)
Query 2: controllare i teli da mare in uso a una certa ora
Idea. I teli in uso all'istante sono quelli delle prenotazioni già entrate () e non ancora uscite ( NULL oppure ). Si somma n_teli e si confronta con STAGIONE.n_teli.
Soluzione del corso (algebra).
con la nota "ricordarsi di applicare ".
Errori della soluzione del corso.
- L'ultima operazione è una differenza insiemistica tra relazioni (
TELITELIUSATI), non una sottrazione tra numeri: toglie dalle tuple diTELIquelle uguali aTELIUSATI, e non calcola i teli disponibili. Per sottrarre si usa una proiezione generalizzata che calcola su una sola tupla ottenuta con un join o prodotto.PRENOTAZIONEeSTAGIONEhanno entrambe un attributon_teli: nel join vanno rinominati (il massimo il numero di teli di una prenotazione).uscita IS NULLrappresenta solo "adesso", cioè le persone ancora presenti: non risponde a "ad una certa ora del giorno" per un istante passato (una prenotazione già uscita alle 13 conta se è le 12).
Algebra corretta. Sia .
(Se nessuna prenotazione è attiva, USATI è vuota e il join perde la stagione: con un left outer join si ha da leggere come 0.) Il controllo chiede che l'ultima colonna sia .
SQL.
SELECT s.anno, s.n_teli AS massimo, COALESCE(SUM(p.n_teli), 0) AS usati,
s.n_teli - COALESCE(SUM(p.n_teli), 0) AS disponibili
FROM stagione AS s
LEFT JOIN prenotazione AS p
ON p.stagione = s.anno AND p.ingresso <= '2012-06-18 12:00'
AND (p.uscita IS NULL OR p.uscita > '2012-06-18 12:00')
WHERE s.anno = 2012
GROUP BY s.anno, s.n_teli;Il LEFT JOIN con le condizioni nella ON (e non nella WHERE) tiene la stagione anche se nessuna prenotazione è attiva. Risultati sui dati: alle 12:00 sono attive le prenotazioni 1 (2 teli) e 3 (2 teli): 4 usati, 1 disponibile; alle 14:30 la 1 è uscita ma è entrata la 2: usano la 2 (1 telo) e la 3 (2 teli): 3 usati, 2 disponibili; alle 08:00 nessuna: 0 usati, 5 disponibili. Il valore della soluzione del corso (uscita IS NULL) darebbe 2 disponibili alle 12:00, sbagliato.
Come vincolo: un trigger. Più utile di un controllo a richiesta è impedire la violazione. In SQLite (e analogamente con BEFORE INSERT e RAISE EXCEPTION in PostgreSQL, vedi SQL avanzato - indici, transazioni e triggerUn file di record può essere heap (inserimento in ordine, ricerca per uguaglianza in media n/2 accessi), ordinato (log2 n), hash (costante, solo uguaglianza) o B-tree (log_F n, anche intervalli). Un indice è una struttura ausiliaria di coppie (chiave K, puntatore P) ordinate, molto più piccola della tabella: l'indice primario è sul campo di ordinamento del file ordinato (una chiave per blocco, non denso, log2(m)+1 accessi), l'indice secondario è su un campo qualsiasi (denso). CREATE [UNIQUE] INDEX, anche su espressioni; PostgreSQL usa tabelle heap con indici secondari B-tree (predefinito) o hash, e PRIMARY KEY e UNIQUE creano un indice implicito. Gli indici velocizzano le letture ma costano nelle scritture e in spazio. Una transazione è una sequenza atomica di operazioni con proprietà ACID (atomicità, consistenza, isolamento, durabilità): START TRANSACTION, COMMIT, SAVEPOINT, ROLLBACK [TO SAVEPOINT]. Un trigger è una regola evento-condizione-azione (ECA): BEFORE/AFTER, ROW/STATEMENT, WHEN, con funzione PL/pgSQL che usa NEW e OLD; serve per vincoli complessi e attributi derivati.SQL avanzato - indici, transazioni e trigger →):
CREATE TRIGGER controlla_teli BEFORE INSERT ON prenotazione
FOR EACH ROW
BEGIN
SELECT RAISE(ABORT, 'teli da mare insufficienti')
WHERE COALESCE((SELECT SUM(p.n_teli) FROM prenotazione p
WHERE p.stagione = NEW.stagione AND p.ingresso <= NEW.ingresso
AND (p.uscita IS NULL OR p.uscita > NEW.ingresso)), 0) + NEW.n_teli
> (SELECT n_teli FROM stagione WHERE anno = NEW.stagione);
END;Provato: alle 15:00, con 3 teli già in uso (prenotazioni 2 e 3) e massimo 5, la richiesta di una nuova prenotazione con 3 teli è rifiutata, con 2 teli è accettata, e una successiva richiesta di 1 telo alle 15:10 è rifiutata (ormai 5 su 5). Il trigger controlla all'istante di ingresso; il conteggio andrebbe rifatto anche quando si modificano ingressi, uscite o teli (trigger anche su UPDATE).
Query 3: l'età media delle persone che hanno prenotato un ombrellone
Soluzione del corso. Si assume una funzione che calcola l'età in anni:
senza aggiungere un attributo età (sarebbe un attributo derivato che invecchia ogni giorno: la data di nascita basta).
Due precisazioni. (1) Ogni cliente ha almeno una prenotazione (cardinalità nell'ER): la soluzione del corso mediava su tutta CLIENTE. Se esistono clienti senza prenotazioni (come E nei dati) vanno esclusi: . (2) "Le persone" si può leggere come persone distinte (ciascuna conta una volta, anche se prenota più volte) oppure come prenotazioni.
SQL. Età alla data di riferimento 2012-06-18 (in PostgreSQL: EXTRACT(YEAR FROM AGE(DATE '2012-06-18', data_nascita)); in SQLite non c'è AGE, si ricostruisce):
-- persone distinte che hanno prenotato
SELECT AVG(eta) FROM (
SELECT cl.documento,
CAST(strftime('%Y','2012-06-18') AS INT) - CAST(strftime('%Y', cl.data_nascita) AS INT)
- (strftime('%m-%d','2012-06-18') < strftime('%m-%d', cl.data_nascita)) AS eta
FROM cliente AS cl
WHERE cl.documento IN (SELECT cliente FROM prenotazione)) AS t;Età: A 22, B 26, C 12 (E ne ha 42 ma non ha prenotato). Media sulle persone distinte che hanno prenotato: . Se invece si media per prenotazione (A ha due prenotazioni): . Includendo anche E, che non ha prenotato: (errore: media su un insieme diverso). Il compito di norma intende la prima lettura; basta dichiararla.
Errori nel materiale del corso (riassunto)
- Query 1: uguaglianza tra timestamp e data (non trova nulla) e
COUNT(*)al posto dei lettini distinti. - Query 2: differenza insiemistica al posto di una sottrazione numerica; attributo
n_teliomonimo non rinominato;uscita IS NULLrisponde solo a "adesso", non a "ad una certa ora". - Query 3: media su tutti i clienti (va bene solo se tutti hanno prenotato).
Versione ripasso
Schema (soluzione del corso): STAGIONE(anno, n_teli, prezzo_ombrellone, prezzo_telo), OMBRELLONE(numero, stagione, prenotato), CLIENTE(documento, data_nascita), PRENOTAZIONE(numero, n_teli, ingresso, uscita, ombrellone, stagione, cliente), LETTINO(numero), COLLOCATO(prenotazione, lettino). Teoria: SQL - interrogazioni, join e aggregazioniUn'interrogazione SQL ha almeno SELECT e FROM; le altre clausole (WHERE, GROUP BY, HAVING, ORDER BY) sono opzionali e HAVING richiede GROUP BY. Ordine logico di valutazione: FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY. SELECT corrisponde a π (ma senza DISTINCT non elimina i duplicati), WHERE a σ, gli alias ad AS/ρ, i join a ⋈ (INNER, LEFT, RIGHT, FULL). WHERE seleziona le righe prima del raggruppamento, HAVING seleziona i gruppi dopo. Le funzioni COUNT, SUM, AVG, MIN, MAX ignorano i NULL (COUNT(*) conta le righe, DISTINCT toglie i duplicati dentro la funzione); con un aggregato o con GROUP BY nella SELECT possono comparire solo colonne di raggruppamento o aggregati. LIKE usa % (qualsiasi sequenza) e _ (un carattere). I confronti con NULL non sono veri: serve IS NULL. Un filtro sulla tabella di destra di un LEFT JOIN va nella clausola ON, non in WHERE, altrimenti il join diventa interno. Con interi COUNT(a)/COUNT(b) è una divisione intera: serve CAST.SQL - interrogazioni, join e aggregazioni →, SQL avanzato - indici, transazioni e triggerUn file di record può essere heap (inserimento in ordine, ricerca per uguaglianza in media n/2 accessi), ordinato (log2 n), hash (costante, solo uguaglianza) o B-tree (log_F n, anche intervalli). Un indice è una struttura ausiliaria di coppie (chiave K, puntatore P) ordinate, molto più piccola della tabella: l'indice primario è sul campo di ordinamento del file ordinato (una chiave per blocco, non denso, log2(m)+1 accessi), l'indice secondario è su un campo qualsiasi (denso). CREATE [UNIQUE] INDEX, anche su espressioni; PostgreSQL usa tabelle heap con indici secondari B-tree (predefinito) o hash, e PRIMARY KEY e UNIQUE creano un indice implicito. Gli indici velocizzano le letture ma costano nelle scritture e in spazio. Una transazione è una sequenza atomica di operazioni con proprietà ACID (atomicità, consistenza, isolamento, durabilità): START TRANSACTION, COMMIT, SAVEPOINT, ROLLBACK [TO SAVEPOINT]. Un trigger è una regola evento-condizione-azione (ECA): BEFORE/AFTER, ROW/STATEMENT, WHEN, con funzione PL/pgSQL che usa NEW e OLD; serve per vincoli complessi e attributi derivati.SQL avanzato - indici, transazioni e trigger →.
- Lettini usati il 18/6/2012.
ingressoè un timestamp: il confronto con la data va fatto sulla parte data,DATE(ingresso) = '2012-06-18', non coningresso = '2012-06-18', che dà zero righe. IlCOUNT(*)conta le coppie (prenotazione, lettino) e il lettino 1 è riusato, quindi servono i distinti: 4 lettini, contro 5 assegnazioni. In algebra si proietta sulettinoe poi si conta.
SELECT COUNT(DISTINCT co.lettino) AS lettini_usati
FROM collocato AS co INNER JOIN prenotazione AS p ON co.prenotazione = p.numero
WHERE DATE(p.ingresso) = '2012-06-18';- Teli in uso a un'ora . Sono in uso le prenotazioni con e con NULL oppure . Il confronto è con
STAGIONE.n_teli. Il corso usauscita IS NULL, che vale solo per "adesso"; la differenza va fatta tra numeri, non tra relazioni. Con ilLEFT JOINe le condizioni inON, la stagione compare anche se non ci sono prenotazioni attive. Alle 12:00 sono attivi 4 teli, quindi 1 disponibile; alle 14:30 sono 3, quindi 2 disponibili.
SELECT s.anno, s.n_teli AS massimo, COALESCE(SUM(p.n_teli), 0) AS usati,
s.n_teli - COALESCE(SUM(p.n_teli), 0) AS disponibili
FROM stagione AS s
LEFT JOIN prenotazione AS p
ON p.stagione = s.anno AND p.ingresso <= '2012-06-18 12:00'
AND (p.uscita IS NULL OR p.uscita > '2012-06-18 12:00')
WHERE s.anno = 2012
GROUP BY s.anno, s.n_teli;- Come vincolo è meglio un trigger
BEFORE INSERTconRAISE(ABORT, ...): provato, con 3 teli già in uso su 5, una richiesta di 3 teli è rifiutata e una di 2 è accettata.
- Età media dei clienti che hanno prenotato. Si assume una funzione e si media su
CLIENTE, senza memorizzare l'età, che invecchia. Va però considerato solo chi ha prenotato, quindi . Con persone distinte: ; per prenotazione: .
Errori del corso
- Query 1: confronto tra timestamp e data, e
COUNT(*)al posto dei lettini distinti. - Query 2: differenza insiemistica invece di una sottrazione;
n_teliomonimo non rinominato;uscita IS NULLrisponde solo a "adesso". - Query 3: media su tutti i clienti, anche quelli senza prenotazioni.
Errori tipici
- Confrontare un timestamp con una data.
- Contare assegnazioni al posto di oggetti distinti.
- Sottrarre relazioni invece di numeri.
- Fare la media su un insieme diverso da quello richiesto.