Esercizio - Interrogazioni sulla piattaforma di streaming musicale (tema d'esame 12 febbraio 2024)
In questa pagina 7
Testo (esame di Basi di Dati del 12 febbraio 2024, II e III parte, dal corso di Basi di Dati, Ing. Informatica UniPD). Degli studenti vogliono progettare una piattaforma per lo streaming di brani musicali. Requisiti:
- ogni utente è iscritto con un nome utente e una password, e può, se vuole, inserire l'anno di nascita e i generi musicali preferiti;
- ogni brano è identificato da un codice, ha un titolo, una durata e un artista di riferimento;
- ogni artista è identificato da un codice, ha un nome, e può essere un gruppo o un artista singolo. Per un gruppo si vuole l'anno di formazione e il cantante leader (sempre definito per un gruppo, ed è un artista singolo; le formazioni non cambiano nel tempo); per un artista singolo nome, cognome, anno di nascita e sesso;
- un brano appartiene a uno o più generi (identificati dal nome, con una descrizione); un genere deve avere almeno un brano;
- per le canzoni ascoltate almeno una volta si vuole sapere quante volte in totale sono state riprodotte dallo stesso utente.
Interrogazioni, in SQL e in algebra relazionale (senza aggiungere attributi ridondanti):
- per ciascun brano, il numero totale di ascolti;
- la band musicale del genere "rock" ascoltata dal maggior numero di utenti (non il maggior numero di ascolti);
- i cantanti leader dei gruppi musicali "pop" dei brani ascoltati dagli utenti nati dopo il 2000.
(Domanda 2, per la lode: quali modifiche allo schema ER e relazionale rendono più efficiente almeno una delle tre query.)
La progettazione ER è 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 →, 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 →, 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 →.
Progettazione ER e schema relazionale della stessa traccia: Esercizio - Progettazione ER e schema relazionale dello streaming di musica (tema d'esame 12 febbraio 2024).
Schema relazionale (dalla soluzione del corso)
Chiavi in grassetto. Nel nome delle tabelle di relazione si usano quelli della soluzione () con il significato indicato.
| Relazione | Attributi | Note |
|---|---|---|
UTENTE |
username, password, anno_nascita | anno_nascita opzionale (può essere NULL) |
GENERE |
nome, descrizione | |
utente UTENTE, genere GENERE |
generi preferiti (N-N) | |
genere GENERE, brano BRANO |
appartenenza brano–genere (N-N); vincolo: ogni brano e ogni genere compaiono almeno una volta | |
BRANO |
codice, titolo, durata, artista ARTISTA |
un solo artista per brano (1-N) |
utente UTENTE, brano BRANO, numero |
numero = quante volte l'utente ha ascoltato il brano (attributo della relazione N-N) |
|
ARTISTA |
codice, tipo, nome, anno_formazione, leader ARTISTA, cognome_c, anno_nascita_c, sesso |
gerarchia gruppo/cantante assorbita nel genitore: per un gruppo valgono anno_formazione e leader; per un cantante valgono cognome_c, anno_nascita_c e sesso (gli altri NULL); leader punta a un cantante (autoriferimento) |
Il suffisso _c distingue gli attributi del cantante da quelli omonimi (anno_nascita di UTENTE): nella soluzione del corso il nome del cantante riusa nome e il cognome diventa cognome_c. Vincoli esterni: se tipo = 'gruppo' allora anno_formazione e leader non sono NULL; leader deve essere un cantante.
Dati inventati. Utenti con anno: u1 1995, u2 2001, u3 2003, u4 2005, u5 NULL. Generi: rock, pop. Cantanti (codice, nome, cognome): 11 Luca Neri, 12 Anna Verdi, 13 Piero Bianchi, 14 Sara Gialli, 15 Elio Rossi. Gruppi (codice, nome, leader): 1 I Fulmini (11), 2 Le Onde (12), 3 Nebbia (13), 4 Alba (14). Brani (codice, titolo, artista): 1 Tuono (1), 2 Lampo (1), 3 Foschia (3), 4 Marea (2), 5 Aurora (4), 6 Solo io (15), 7 Ponte (2), 8 Inascoltato (1), 9 Bruma (3), 10 Foschia 2 (3). : rock: 1, 2, 3, 7, 8, 9, 10; pop: 4, 5, 6, 7. (utente, brano, numero): u1: (1,3), (2,2); u2: (1,1), (3,4), (4,2), (9,2), (10,1); u3: (3,1), (5,5), (7,1), (9,1), (10,2); u4: (4,1), (6,3); u5: (2,1).
Query 1: il numero totale di ascolti di ciascun brano
Idea. Il numero di ascolti di un brano è la somma, sugli utenti, di numero in : un raggruppamento per brano.
Algebra. (grado 2, una tupla per brano ascoltato).
SQL.
SELECT brano, SUM(numero) AS totale
FROM r3
GROUP BY brano;Risultato: brano 1: 4; 2: 3; 3: 5; 4: 3; 5: 5; 6: 3; 7: 1; 9: 3; 10: 3. Il brano 8 (mai ascoltato) non compare: con la soluzione del corso si ottengono solo i brani con almeno un ascolto. Se si vuole ogni brano, anche con totale 0, si parte da BRANO con un left join:
SELECT b.codice, b.titolo, COALESCE(SUM(r.numero), 0) AS totale
FROM brano AS b LEFT JOIN r3 AS r ON r.brano = b.codice
GROUP BY b.codice, b.titolo;(8 compare con 0; COALESCE trasforma il NULL dell'aggregato su un gruppo vuoto in 0.) Nota: il brano è chiamato brano in e codice in BRANO; nella soluzione del corso la query scrive codice per l'attributo di , un'incoerenza di nomi.
Query 2: la band rock ascoltata dal maggior numero di utenti
Idea. Si passa da rock brani rock band autrici ascolti, poi si conta per band e si prende il massimo. Il testo precisa "il maggior numero di utenti (non il maggior numero di ascolti)": si contano gli utenti distinti per band.
Soluzione del corso (algebra).
CREATE VIEW Ascolti AS
SELECT a.codice AS artista, COUNT(*) AS totale
FROM R2 AS r INNER JOIN BRANO AS b ON r.brano = b.codice
INNER JOIN ARTISTA AS a ON b.codice_artista = a.codice
INNER JOIN R3 AS r3 ON b.codice = r3.brano
WHERE genere = 'Rock' AND tipo = 'band'
GROUP BY a.codice;
SELECT artista, totale FROM Ascolti WHERE totale = (SELECT MAX(totale) FROM Ascolti);Errore nella soluzione del corso.
COUNT(*)conta le righe del join, cioè le coppie (utente, brano) di : un utente che ha ascoltato due brani della stessa band viene contato due volte. Il testo chiede il numero di utenti, non di ascolti (e nemmeno di coppie). Sui dati: I Fulmini (1) ha 3 utenti (, , ) ma 4 coppie; Nebbia (3) ha solo 2 utenti (, ) ma 6 coppie (ciascuno ascolta 3 brani della band). La query del corso restituisce Nebbia (6), quella corretta I Fulmini (3). Lo stesso errore è nell'algebra: dopo il join conta le tuple del join.
Soluzione corretta (SQL). Si contano gli utenti distinti per band:
SELECT a.codice, a.nome, COUNT(DISTINCT r3.utente) AS utenti
FROM r2 AS r
INNER JOIN brano AS b ON r.brano = b.codice
INNER JOIN artista AS a ON b.artista = a.codice
INNER JOIN r3 ON b.codice = r3.brano
WHERE r.genere = 'rock' AND a.tipo = 'gruppo'
GROUP BY a.codice, a.nome
HAVING COUNT(DISTINCT r3.utente) = (
SELECT MAX(n) FROM (
SELECT COUNT(DISTINCT r3.utente) AS n
FROM r2 AS r INNER JOIN brano AS b ON r.brano = b.codice
INNER JOIN artista AS a ON b.artista = a.codice
INNER JOIN r3 ON b.codice = r3.brano
WHERE r.genere = 'rock' AND a.tipo = 'gruppo'
GROUP BY a.codice) AS t);(Più leggibile con una vista come nella soluzione del corso, sostituendo COUNT(*) con COUNT(DISTINCT r3.utente).) Risultato: I Fulmini, 3 utenti. Le Onde ha 1 utente (via il brano 7, che è sia pop sia rock), Nebbia 2.
Algebra corretta. In algebra si ottengono i distinti con una proiezione (che elimina i duplicati) sulle coppie (band, utente) prima di contare:
Provato con un interprete di algebra: stesso risultato dell'SQL (I Fulmini).
Query 3: i cantanti leader dei gruppi "pop" dei brani ascoltati da utenti nati dopo il 2000
Idea. Utenti con anno_nascita > 2000 i loro ascolti () i brani pop () l'artista del brano, che deve essere un gruppo il suo leader (un altro artista).
Soluzione del corso (algebra).
La condizione leader IS NOT NULL serve per tenere i gruppi (i cantanti singoli non hanno leader); l'espressione restituisce i codici dei leader.
Imprecisioni della soluzione del corso. (1) La versione SQL è priva di
DISTINCT: un leader compare una volta per ogni ascolto di un brano pop del suo gruppo (la proiezione dell'algebra elimina i duplicati,SELECTno): sui dati la query restituisce 4 righe (12, 14, 12, 12) invece di 2 leader. (2)anno_nascitapuò essere ambiguo se ancheARTISTAha un attributo con quel nome (quianno_nascita_c): si qualifica con l'alias. (3) Il risultato è il solo codice del leader: per "i cantanti leader" è meglio un join conARTISTAper ottenere nome e cognome. (4)anno_nascita > 2000scarta l'utente con anno NULL (u5), come è giusto.
SQL corretto.
SELECT DISTINCT l.codice, l.nome, l.cognome_c
FROM utente AS u
INNER JOIN r3 ON u.username = r3.utente
INNER JOIN r2 ON r3.brano = r2.brano
INNER JOIN brano AS b ON r2.brano = b.codice
INNER JOIN artista AS g ON b.artista = g.codice
INNER JOIN artista AS l ON g.leader = l.codice
WHERE u.anno_nascita > 2000 AND r2.genere = 'pop' AND g.tipo = 'gruppo';artista compare due volte: g (il gruppo del brano) e l (il suo leader), collegati dall'autoriferimento g.leader = l.codice (è un self-join). Risultato: Anna Verdi (leader di Le Onde, ascoltata da , , tramite i brani 4 e 7) e Sara Gialli (leader di Alba, ascoltata da tramite il brano 5). Il brano 6 è pop ma di un cantante singolo (Elio Rossi): non è di un gruppo e non produce un leader.
Algebra corretta. Gli stessi passi con la rinomina del secondo uso di ARTISTA e una proiezione finale su codice, nome e cognome del leader (provata: stesso risultato).
Domanda 2 (per la lode): modifiche per rispondere più in fretta
- Query 1: serve la somma sulle righe di per brano. Un indice su (o, poiché la chiave è , un indice con
branoin prima posizione) rende il raggruppamento una scansione ordinata. In alternativa un attributo derivatototale_ascoltiinBRANO, da aggiornare a ogni ascolto con un trigger (letture immediate, ma aggiornamenti più cari e ridondanza). - Query 2: serve contare gli utenti distinti per band: un attributo derivato
n_ascoltatoriinARTISTA(aggiornato quando un utente ascolta per la prima volta un brano della band) evita tre join e unCOUNT(DISTINCT); conviene se la query è frequente e gli ascolti, rispetto alle letture, sono meno frequenti. In alternativa la vista materializzata dei conteggi per band. - Query 3: il percorso utente brano genere artista leader è lungo: si può spostare l'attributo
leader(o una colonnagenere_principale) dove serve, o indicizzareUTENTE(anno_nascita)e .
Errori nel materiale del corso (riassunto)
- Query 2:
COUNT(*)conta coppie (utente, brano) e non utenti: dà Nebbia (6) invece di I Fulmini (3). - Query 3: SQL senza
DISTINCT(4 righe invece di 2 leader) e risultato con solo il codice del leader. - Query 1: il brano mai ascoltato manca dal risultato (accettabile, ma va dichiarato); incoerenza
codice/branotra le tabelle.
Versione ripasso
Schema: UTENTE(username, password, anno_nascita), GENERE(nome, descrizione), (utente, genere), (genere, brano), BRANO(codice, titolo, durata, artista), (utente, brano, numero), ARTISTA(codice, tipo, nome, anno_formazione, leader, cognome_c, anno_nascita_c, sesso). Il gruppo ha leader che punta a un cantante. 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 - 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 →.
- Ascolti per brano. . Risultato: brano 1: 4; 2: 3; 3: 5; 4: 3; 5: 5; 6: 3; 7: 1; 9: 3; 10: 3. Il brano 8 non è ascoltato e non compare; con un
LEFT JOINdaBRANOeCOALESCEcompare con 0.
SELECT brano, SUM(numero) AS totale FROM r3 GROUP BY brano;
SELECT b.codice, b.titolo, COALESCE(SUM(r.numero), 0) AS totale
FROM brano AS b LEFT JOIN r3 AS r ON r.brano = b.codice GROUP BY b.codice, b.titolo;- Band rock ascoltata da più utenti. Si conta per band il numero di utenti distinti, poi si prende il massimo. Il conteggio va fatto su
COUNT(DISTINCT utente): ilCOUNT(*)del corso conta coppie (utente, brano) e dà Nebbia con 6 invece di 2 utenti. Risultato corretto: I Fulmini, 3 utenti (u1, u2, u5). Le Onde ha 1 utente e Nebbia 2.
SELECT a.codice, a.nome, COUNT(DISTINCT r3.utente) AS utenti
FROM r2 AS r INNER JOIN brano AS b ON r.brano = b.codice
INNER JOIN artista AS a ON b.artista = a.codice
INNER JOIN r3 ON b.codice = r3.brano
WHERE r.genere = 'rock' AND a.tipo = 'gruppo'
GROUP BY a.codice, a.nome;- Il massimo si ottiene con
HAVINGe una tabella derivata, come nelle query del corso. In algebra si fa prima la proiezione su (band, utente), che elimina i duplicati, e poi il conteggio.
- Leader dei gruppi pop, per brani ascoltati da utenti nati dopo il 2000. Il percorso è utente, , con genere pop, brano, gruppo, leader. Il leader è un altro record di
ARTISTA, quindi serve un self-join con aliasg(gruppo) el(leader). Risultato: Anna Verdi (leader di Le Onde, ascoltata da u2, u3, u4) e Sara Gialli (leader di Alba, ascoltata da u3). Il brano 6, pop ma di un cantante singolo, non produce leader.
SELECT DISTINCT l.codice, l.nome, l.cognome_c
FROM utente AS u
INNER JOIN r3 ON u.username = r3.utente
INNER JOIN r2 ON r3.brano = r2.brano
INNER JOIN brano AS b ON r2.brano = b.codice
INNER JOIN artista AS g ON b.artista = g.codice
INNER JOIN artista AS l ON g.leader = l.codice
WHERE u.anno_nascita > 2000 AND r2.genere = 'pop' AND g.tipo = 'gruppo';- Senza
DISTINCTil leader compare una volta per ogni ascolto: 4 righe invece di 2. Il corso non lo mette. - In algebra la rinomina del secondo
ARTISTA(per esempio ) è necessaria; poi si proietta su codice, nome e cognome del leader, che elimina i duplicati.
Domanda per la lode: indici su , e UTENTE(anno_nascita); oppure attributi derivati come totale_ascolti o n_ascoltatori, aggiornati da un trigger, o una vista materializzata. Lettura più veloce, scrittura più cara, rischio di incoerenza.
Errori tipici
- Contare coppie invece di utenti distinti.
- Dimenticare
DISTINCTdopo un join con più righe per leader. - Confondere il leader (codice) con il suo nome.