Esercizio - Interrogazioni sulla compagnia di treni (tema d'esame 17 giugno 2013)
In questa pagina 6
Testo (compito d'esame del 17 giugno 2013, "Compagnia treni", dal corso di Basi di Dati, Ing. Informatica UniPD). La compagnia Romolo vuole gestire rifornimenti e composizione dei treni:
- ogni treno (composizione) è formato da una o due motrici (in quest'ultimo caso una in testa e una in coda) e da un certo numero di carrozze;
- le motrici hanno un motore, una strumentazione di guida e un numero di posizioni di guida; le carrozze hanno un numero di posti, un numero di bagni, una disposizione su uno o due piani; entrambe hanno una lunghezza, un peso e un tipo di attacco;
- ci sono fornitori (con partita iva) che riforniscono la compagnia di pezzi; ogni pezzo ha un prezzo, diverso a seconda del fornitore; per ogni veicolo si vuole sapere quale fornitore fornisce quale pezzo;
- per ogni tratta (origine, destinazione) coperta si vuole sapere la composizione utilizzata: non ci sono due treni per la stessa coppia di città.
Interrogazioni, in algebra relazionale e in SQL:
- il fornitore che fornisce più pezzi degli altri;
- il costo complessivo di ciascuna composizione;
- la tratta dei treni che hanno due motrici fornite dal fornitore
f1(almeno un pezzo per ciascuna motrice, non necessariamente tutti).
La progettazione ER è nell'esercizio di progettazione della stessa traccia. Qui si ricava lo schema relazionale e si risolvono le tre query. Teoria: 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 - 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 - query nidificate, operatori insiemistici e visteUna sottoquery è una SELECT tra parentesi dentro un'altra: nella FROM (tabella derivata, con alias obbligatorio in PostgreSQL) oppure nella WHERE, come valore scalare con =, <, ..., come insieme con IN / NOT IN, ANY, ALL, oppure con EXISTS / NOT EXISTS. Una sottoquery correlata usa colonne della query esterna e va rivalutata per ogni riga. Le funzioni di aggregazione non si possono usare in WHERE: il massimo si ottiene con una sottoquery scalare (WHERE n = (SELECT MAX(n) ...)), con una vista o una CTE. NOT IN con una sottoquery che contiene NULL non restituisce mai righe: meglio NOT EXISTS, EXCEPT o LEFT JOIN ... IS NULL. "Per tutti" (divisione) = doppio NOT EXISTS o HAVING COUNT(DISTINCT ...) = numero totale. Gli operatori insiemistici UNION, INTERSECT, EXCEPT richiedono query compatibili e di default eliminano i duplicati (ALL li mantiene). Una vista è una tabella virtuale definita da una query (CREATE VIEW, DROP VIEW): semplifica le query, riusa sottoquery, nasconde i dati; la vista materializzata invece memorizza il risultato.SQL - query nidificate, operatori insiemistici e viste →.
Progettazione ER e schema relazionale della stessa traccia: Esercizio - Progettazione ER e schema relazionale della compagnia di treni (tema d'esame 17 giugno 2013).
Schema relazionale
Il PDF della soluzione contiene solo lo schema ER e le espressioni di algebra: lo schema relazionale qui sotto è ricavato dal diagramma ER (veicolo con tipo, scelta 2 del corso: genitore che assorbe i figli) con le regole di traduzione di Ristrutturazione dello schema ER e traduzione nel modello relazionaleLa progettazione logica ha due passi. 1) Ristrutturazione dello schema ER, per togliere i costrutti che il modello relazionale non esprime: analisi delle ridondanze (attributi derivati e cicli di relazioni, decisa con volume dei dati e carico, tabelle di frequenza e accessi), eliminazione degli attributi multivalore (nuova entità), composti (appiattiti o nuova entità), delle IS-A (relazione con partecipazione (1,1) dalla sottoclasse) e delle generalizzazioni (opzione 1 relazioni, 2 accorpamento nel padre con attributo discriminante, 3 accorpamento nei figli, solo se completa e disgiunta), scelta degli identificatori principali (niente cicli di identificazione esterna), riscrittura dei vincoli esterni, partizionamento e accorpamento di entità. 2) Traduzione quasi automatica nel modello relazionale: ogni entità diventa una relazione con la chiave primaria data dall'identificatore principale (anche esterno); una relazione molti-a-molti diventa una relazione con le chiavi delle entità; una uno-a-molti con partecipazione obbligatoria si fonde con l'entità dal lato max 1 con una chiave esterna; con partecipazione opzionale si fonde (NULL) o si crea una relazione a parte; le uno-a-uno si fondono sul lato con partecipazione obbligatoria; le ternarie seguono la stessa logica con le chiavi di tutte le entità.Ristrutturazione dello schema ER e traduzione nel modello relazionale →. Chiavi in grassetto.
| Relazione | Attributi | Da dove viene |
|---|---|---|
FORNITORE |
piva | entità |
PEZZO |
codice, descrizione, categoria | entità |
OFFRE |
fornitore FORNITORE, pezzo PEZZO, prezzo |
relazione N-N con attributo: il prezzo dipende dalla coppia |
COMPOSIZIONE |
codice, descrizione, (costo) | entità; costo è un attributo derivato, ammesso per la seconda query |
VEICOLO |
id, peso, lunghezza, attacco, tipo, n_guida, n_posti, disposizione, n_bagni, composizione COMPOSIZIONE, testa |
generalizzazione motrice/carrozza assorbita nel genitore (tipo); gli attributi delle sole motrici (n_guida, testa) o delle sole carrozze (n_posti, disposizione, n_bagni) sono NULL per l'altro tipo; le relazioni 1-N "fa parte di" (0,1)-(1,N) diventano la chiave esterna composizione; testa è l'attributo della relazione con le motrici |
VENDE |
fornitore, pezzo, veicolo VEICOLO; OFFRE |
relazione ternaria fornitore–pezzo–veicolo |
CITTA |
id, nome | entità |
TRATTA |
origine CITTA, destinazione CITTA, composizione COMPOSIZIONE |
entità identificata esternamente dalle due città; composizione può essere NULL (tratta non ancora servita) |
Vincoli esterni (non esprimibili con chiavi): una composizione ha una o due motrici; la motrice testa è unica; la coppia di VENDE deve esistere in OFFRE (altrimenti non si conosce il prezzo).
Dati inventati. Fornitori f1, f2, f3; OFFRE (fornitore, pezzo, prezzo): f1: p1 1000, p2 500, p3 200, p5 100; f2: p1 900, p3 220, p4 300, p5 110, p6 50; f3: p6 60. Composizioni: K1 Padova-Venezia (motrici m1 in testa, m2; carrozze c1, c2), K2 Milano-Venezia (m3, m4; c3, c4), K3 Roma-Napoli (m5; c5), K4 Torino-Milano (m6, m7; c6). TRATTA: Padova-Venezia: K1; Milano-Venezia: K2; Roma-Napoli: K3; Torino-Milano: K4; Venezia-Trieste: nessuna. VENDE (fornitore, pezzo, veicolo): f1/p1/m1, f1/p2/m2, f1/p1/m3, f2/p1/m4, f1/p1/m5, f1/p1/m6, f1/p2/m6, f2/p1/m7, f1/p3/c1, f2/p4/c1, f2/p3/c2, f1/p5/c3, f2/p5/c4, f2/p6/c5, f3/p6/c6, f1/p5/m1, f2/p5/m2.
Query 1: il fornitore che fornisce più pezzi degli altri
Idea. "Fornisce" i pezzi nel senso di OFFRE (sono i pezzi che mette in catalogo): si conta per fornitore e si prende il massimo. È il pattern del massimo: (1) conteggio per gruppo, (2) massimo dei conteggi, (3) join per tenere i gruppi con quel valore.
Algebra.
Nella soluzione del corso l'ultimo passo è scritto , dove MAXF è una relazione e non un valore: è un'abbreviazione, la forma corretta è il join (o il prodotto seguito dalla selezione), che il corso indica come alternativa valida.
SQL.
SELECT fornitore, COUNT(*) AS npezzi
FROM offre
GROUP BY fornitore
HAVING COUNT(*) = (SELECT MAX(n)
FROM (SELECT COUNT(*) AS n FROM offre GROUP BY fornitore) AS t);Risultato sui dati: f2 con 5 pezzi (f1 ne ha 4, f3 uno). In caso di parità la query restituisce tutti i fornitori a pari merito.
Commenti.
- Si può leggere "fornisce più pezzi" anche come pezzi realmente venduti per i veicoli (
VENDE): conCOUNT(DISTINCT pezzo)suVENDEsi ottengono f1: 4, f2: 5, f3: 1: stesso risultato qui, ma i due significati possono differire. Occorre dichiarare quello usato. - Un fornitore senza offerte (cardinalità nell'ER) non compare in
OFFRE: la query lo ignora, il che va bene per il massimo. Per elencare anche loro, con conteggio 0, si userebbe unLEFT JOINdaFORNITOREeCOUNT(o.pezzo).
Query 2: il costo complessivo di ciascuna composizione
Con l'attributo derivato costo (la scelta della soluzione, comoda per questa query):
È una semplice proiezione, ma il prezzo da pagare è la ridondanza: costo va ricalcolato (a mano, da un trigger o dall'applicazione) ogni volta che cambiano i veicoli della composizione, i pezzi venduti o i prezzi di OFFRE. Per questo il testo del corso lo marca come "attributo derivato (per Q2)".
Senza l'attributo derivato, il costo si calcola: la composizione contiene i veicoli, per ogni veicolo ci sono le righe di VENDE (quali pezzi, da quale fornitore), e il prezzo di ciascun pezzo è in OFFRE per la coppia (fornitore, pezzo). Il costo è la somma di tutti questi prezzi.
SELECT cm.codice, cm.descrizione, SUM(o.prezzo) AS costo
FROM composizione AS cm
INNER JOIN veicolo AS v ON v.composizione = cm.codice
INNER JOIN vende AS ve ON ve.veicolo = v.id
INNER JOIN offre AS o ON o.fornitore = ve.fornitore AND o.pezzo = ve.pezzo
GROUP BY cm.codice, cm.descrizione;Risultato sui dati: K1: 2430, K2: 2110, K3: 1050, K4: 2460. Passaggi di K1: m1 ha p1 di f1 (1000) e p5 di f1 (100), m2 ha p2 di f1 (500) e p5 di f2 (110): 1100 + 610; c1 ha p3 di f1 (200) e p4 di f2 (300): 500; c2 ha p3 di f2: 220; totale .
La join con OFFRE deve essere sulla coppia (fornitore, pezzo): il prezzo cambia con il fornitore, quindi unire solo per pezzo darebbe un prodotto sbagliato (si sommerebbero i prezzi di tutti i fornitori di quel pezzo). Il costo non comprende il "prezzo" dei veicoli in sé: il testo non lo prevede, conta solo la fornitura dei pezzi.
Query 3: la tratta dei treni con due motrici fornite da f1
Idea. Si cercano le composizioni in cui due motrici distinte hanno almeno un pezzo fornito da f1. "Almeno un pezzo per ciascuna motrice": basta una riga di VENDE per motrice. Il punto delicato è che una motrice può avere più pezzi di f1 (più righe in VENDE): si devono contare le motrici distinte, non le righe.
Algebra (soluzione del corso). Con la scelta del veicolo unico:
(join naturale: l'unico attributo in comune è composizione).
La proiezione che costruisce MOT elimina i duplicati: una motrice con due pezzi di f1 genera due righe in MOTRICI ma una sola in MOT. Perciò COUNT(*) su MOT conta le motrici distinte di f1 per composizione. La condizione scarta sia le composizioni con una sola motrice (non potranno mai arrivare a 2), sia quelle in cui f1 fornisce pezzi a una sola delle due motrici: la soluzione del corso lo nota ("evita di aggiungere attributi derivati extra"). Il risultato è una tratta per composizione, e il join con TRATTA dà origine e destinazione.
SQL.
SELECT t.origine, t.destinazione
FROM tratta AS t
WHERE t.composizione IN (
SELECT v.composizione
FROM veicolo AS v INNER JOIN vende AS ve ON ve.veicolo = v.id
WHERE v.tipo = 'motrice' AND ve.fornitore = 'f1'
GROUP BY v.composizione
HAVING COUNT(DISTINCT v.id) = 2);Risultato sui dati: la sola tratta Padova-Venezia (K1: m1 ha pezzi di f1, m2 anche). K2 ha una sola motrice di f1 (m3, mentre m4 è di f2), K3 una sola motrice in tutto, K4 ha m6 con due pezzi di f1 ma m7 nessuno.
La trappola. Con
COUNT(*)al posto diCOUNT(DISTINCT v.id)la query restituirebbe Torino-Milano (K4: le due righe diVENDEdi m6 sembrano "due motrici") e scarterebbe Padova-Venezia (m1 ha due righe, m2 una: tre). Provato sui dati. La proiezione dell'algebra evita l'errore; in SQL serveDISTINCTdentro l'aggregato.
Un'alternativa senza raggruppamento è un self-join sulle motrici della stessa composizione con a.id < b.id ed EXISTS su VENDE per ciascuna (stesso risultato, provato).
Errori nel materiale del corso (riassunto)
- Query 1: confronta un attributo con una relazione; vale solo come abbreviazione del join.
- Query 2: la soluzione presuppone l'attributo derivato
costo; la versione senza derivato qui sopra mostra che il costo si ricava daVENDEeOFFRE. - Query 3: la traduzione diretta in SQL richiede
COUNT(DISTINCT ...); conCOUNT(*)si contano le righe diVENDEe non le motrici. Inoltre l'espressione usatipo = 'motrice', valida solo con la scelta di ristrutturazione 2 (veicolo unico).
Versione ripasso
Schema (ricavato dall'ER, con veicolo unico): FORNITORE(piva), PEZZO(codice, descrizione, categoria), OFFRE(fornitore, pezzo, prezzo), COMPOSIZIONE(codice, descrizione, costo) con costo derivato, VEICOLO(id, peso, lunghezza, attacco, tipo, n_guida, n_posti, disposizione, n_bagni, composizione, testa), VENDE(fornitore, pezzo, veicolo), CITTA(id, nome), TRATTA(origine, destinazione, composizione). Teoria: 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 - 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 →.
- Fornitore con più pezzi. Si conta in
OFFREper fornitore, si prende il massimo e si fa il join sul massimo: . Il del corso è un'abbreviazione del join. Risultato: f2 con 5 pezzi (f1 ne ha 4, f3 uno). In caso di parità restituisce tutti.
SELECT fornitore, COUNT(*) AS npezzi FROM offre GROUP BY fornitore
HAVING COUNT(*) = (SELECT MAX(n) FROM (SELECT COUNT(*) AS n FROM offre GROUP BY fornitore) AS t);- Costo di ogni composizione. Con l'attributo derivato
costoè una proiezione, ma il dato va mantenuto a mano. Senza, si fa il join diCOMPOSIZIONE,VEICOLO,VENDEeOFFREsulla coppia (fornitore, pezzo) e si somma il prezzo per composizione. Risultato: K1 2430, K2 2110, K3 1050, K4 2460. Conto di K1: m1 , m2 , c1 , c2 ; totale .
SELECT cm.codice, cm.descrizione, SUM(o.prezzo) AS costo
FROM composizione AS cm
INNER JOIN veicolo AS v ON v.composizione = cm.codice
INNER JOIN vende AS ve ON ve.veicolo = v.id
INNER JOIN offre AS o ON o.fornitore = ve.fornitore AND o.pezzo = ve.pezzo
GROUP BY cm.codice, cm.descrizione;- Tratta con due motrici fornite da f1. Si proietta su (composizione, veicolo, fornitore), eliminando i duplicati, poi si conta: , con selezione e join naturale con
TRATTA. Risultato: Padova-Venezia (K1).
SELECT t.origine, t.destinazione FROM tratta AS t
WHERE t.composizione IN (SELECT v.composizione FROM veicolo AS v
INNER JOIN vende AS ve ON ve.veicolo = v.id
WHERE v.tipo = 'motrice' AND ve.fornitore = 'f1'
GROUP BY v.composizione HAVING COUNT(DISTINCT v.id) = 2);- Trappola: con
COUNT(*)si contano le righe diVENDE. Una motrice con due pezzi di f1 vale due, e la query dà Torino-Milano (K4, m6 con due pezzi, m7 nessuno) invece di Padova-Venezia.
Errori tipici
- Join con
OFFREsolo supezzo: si sommano i prezzi di tutti i fornitori. COUNT(*)al posto diCOUNT(DISTINCT).- Dimenticare di mantenere l'attributo derivato
costo. - Confrontare un attributo con una relazione, come nel del corso.