Primo Appello a.a Gennaio 2012
Call Me (on the line) Una base di dati supporta una giuria tecnica che deve votare per delle canzoni votate anche tramite un sistema di televoto. Si specifichi un insieme di trigger per la gestione automatica delle votazioni, in base al seguente regolamento. Il televoto registra i voti ad ogni canzone per un intervallo di tempo limitato (gestito fuori dalla base di dati): dalla stessa utenza telefonica si accettano solo i primi 10 voti, che concorrono a formare il numero totale dei televoti alla canzone (tabella V OTAZIONE ). In parallelo, ciascun giurato esprime un voto da 1 a 10 per ogni canzone. Dopo ogni voto della giuria il sistema aggiorna anche il numero-voti e il totale-punti della giuria (sempre nella tabella V OTAZIONE ). Il processo di voto termina quando tutti i membri della giuria hanno espresso un voto per tutte le canzoni; a questo punto si determina la canzone vincitrice (sono ammessi pareggi), assegnando un peso del 50% ai punteggi totali dei due sistemi di votazione. Si assuma che la tabella V OTAZIONE contenga già tutte le canzoni con tutti i punteggi a 0 e con VotoComposto=NULL. M EMBROGIURIA (IDmembro, Cognome, Nome) C ANZONE (IDcanzone, Titolo, Autore, Cantante) V OTOGIURIA (IDmembro, IDcanzone, Voto) T ELEVOTO (Telefono, Ora, IDcanzone) V OTAZIONE (IDcanzone, NumTelevoti, NumVotiGiuria, TotPuntiGiuria, VotoComposto, Vincitrice) 2
Le difficoltà sono due: stabilire una ragionevole funzione di aggregazione dei voti di diversa natura e riconoscere la fine delle votazioni. Per la prima questione si può adottare una soluzione estremamente semplice, come sommare i voti da casa e i punti-giuria, oppure ad esempio normalizzare a 10 il voto per la canzone più televotata e assegnare un voto proporzionale per le altre meno televotate, facendo poi la media tra questo voto normalizzato e la media dei voti della giuria. Nel primo caso si può fare il conto dopo ogni “finestra temporale”, nel secondo occorre attendere che tutto il rpcesso sia concluso. Per intercettare la fine delle votazioni possiamo confrontare il numero di voti espressi dalla giuria per ogni canzone con la cardinalità della tabella M EMBROGIURIA, basandoci ad esempio sull’ipotesi che la giuria esprima i suoi voti solo al termine della “finestra temporale” del televoto, e innescare il calcolo del vincitore solo quando tutte le canzoni hanno avuto il voto finale. 3
create trigger ImpedisciTruffaTelevoto before insert on T ELEVOTO for each row when 10 <= ( select count(*) // respinge i voti dopo il decimo from T ELEVOTO where Telefono = new.Telefono ) rollback; create trigger AccumulaTelevoto after insert on T ELEVOTO // accetta quelli che passano il controllo “before” for each row begin update V OTAZIONE set NumTelevoti = NumTelevoti + 1 where IDcanzone = new.IDcanzone end; 4
In alternativa, il comportamento dei due trigger precedenti si può riassumere in un solo trigger che prima di applicare l’incremento controlla il numero di voti già espressi create trigger AccumulaTelevotoControllando after insert on T ELEVOTO for each row when 10 >= ( select count(*) from T ELEVOTO where Telefono = new.Telefono ) begin update V OTAZIONE set NumTelevoti = NumTelevoti + 1 where IDcanzone = new.IDcanzone end; 5
create trigger AccumulaVotoGiuria after insert on V OTOGIURIA for each row begin update V OTAZIONE set NumVotiGiuria= NumVotiGiuria + 1, set TotPuntiGiuria= TotPuntiGiuria + new.Voto where IDcanzone = new.IDcanzone end; Per ovvi motivi di fiducia nella competenza della giuria (o nell’infrastruttura di voto) non controlliamo che il voto tecnico sia effettivamente compreso tra 1 e 10. Volendo, si potrebbe aggiungere un trigger per questo. 6
L’opzione in assoluto più semplice per il calcolo del punteggio composto è fare la somma tra il totale dei punti-giuria e dei voti da casa. Questo si può fare nonappena la giuria termina di votare per una specifica canzone (e il televoto è chiuso). create trigger CalcolaVotoFinale after update of NumVotiGiuria on V OTAZIONE for each row when new.NumVotiGiuria = ( select count(*) // la giuria ha finito fine televoto from M EMBROGIURIA ) begin update V OTAZIONE set VotoComposto = 0,5 * NumTelevoti + 0,5 * TotPuntiGiuria where IDcanzone = new.IDcanzone end; Se però i televoti sono significativamente più di dieci volte il numero dei giurati, il confronto diventa molto unfair (il contributo della giuria “scompare”). Possiamo allora assegnare un voto pari a 10 punti alla canzone più televotata, e punteggi proporzionali alle altre. 7
create trigger CalcolaVotoFinale after update of NumVotiGiuria on V OTAZIONE for each row when 0 = ( select count(*) from V OTAZIONE where NumVotiGiuria < ( select count(*) from M EMBROGIURIA ) ) begin select max(NumTelevoti)/10 into UnitTelevoti // un decimo del numero di voti presi dalla from V OTAZIONE // canzone più televotata, il cui totale di voti // equivale a un 10 assegnato dal pubblico. update V OTAZIONE set VotoComposto = 0,5 * NumTelevoti / UnitTelevoti + 0,5 * TotPuntiGiuria / NumVotiGiuria where IDcanzone = new.Idcanzone end; 8 Solo alla fine di tutto il processo di voto, perché servono tutti i televoti per trovare il massimo e fare le proporzioni
Per decretare la vincitrice occorre che tutti i voti composti siano stati calcolati create trigger DecretaVincitrice after update of VotoComposto on V OTAZIONE for each row when not exists ( select * // scatta solo alla fine dell’ultimo aggiornamento from V OTAZIONE where VotoComposto is null ) begin update V OTAZIONE set Vincitrice = false; // dapprima tutti a false, se non già inizializzato così update V OTAZIONE set Vincitrice = true where VotoComposto = ( select max(VotoComposto) // può non essere unico from V OTAZIONE ); // eventuale gestione del pari-merito, privilegiando ad esempio il voto della giuria, // e in caso di ulteriore parimerito... mettendo all’asta il titolo ! end; 9
In alternativa, possiamo sfruttare la semantica “for each statemet”, che combinato con la seconda versione del trigger “CalcolaVotoFinale”, eseguito una sola volta, non richiede controlli sul fatto che l’aggiornamento sia l’ultimo create trigger DecretaVincitrice after update of VotoComposto on V OTAZIONE for each statement // scatta dopo l’unico comando di aggiornamento begin update V OTAZIONE set Vincitrice = false; // dapprima tutti a false, se non già inizializzato così update V OTAZIONE set Vincitrice = true where VotoComposto = ( select max(VotoComposto) // può non essere unico from V OTAZIONE ); if ( 2 <= ( select count(*) from V OTAZIONE where Vincitrice = true ) ) update V OTAZIONE set Vincitrice = false where TotPuntiGiuria < ( select max(TotPuntiGiuria ) from V OTAZIONE where Vincitrice = true) end; 10
(ctrl-z) n Il seguente schedule appartiene alla classe TS-2versioni, mentre non appartiene alla classe TS-monoversione: r 2 (X) w 2 (X) r 1 (X) Estendere il noto diagramma di inclusione delle classi VSR, CSR e 2PL con la rappresentazione delle classi TS-2versioni e TS-3versioni. Mostrare inoltre un esempio di schedule che appartiene alla classe TS-3 e non alla TS-2. Il fatto di poter avere anche una sola versione in più rispetto all’ultima permette di “violare” il principio della relazione “legge-da” e rende quindi non confrontabili le classi TS-n con la classe VSR per ogni n>1. Pertanto il “famoso” schedule w 1 w 2 r 1, che non è serializzabile, è accettabile da qualsiasi sistema TS con più di una versione. Detto questo, è invece evidente che all’aumentare del numero di versioni disponibili aumenta anche il numero di schedule riconosciuti. Lo schedule w 1 w 2 w 3 r 1 appartiene a TS-3 ma non a TS-2 Più in generale, lo schedule w 1 w 2 w 3... w n-1 w n r 1 appartiene a TS-n ma non a TS-(n-1) 11
Serial VSR CSR 2PL TS(mono) TS-2 TS-3 TS-n... 12
ObermarkT Wassily Kandinsky "Houses in Murnau on Obermarkt"
Obermark Su una base dati distribuita su 3 nodi (α, β, γ) sono in esecuzione sei transazioni T 1...T 6, che operano sulle risorse A...F, così allocate: A,B,C sul nodo α, D sul nodo β e E,F sul nodo γ. Le operazioni delle transazioni sono state registrate in questo ordine: r 1 (E), r 2 (D), r 3 (A), r 2 (C), w 1 (B), r 4 (B), w 4 (A), r 3 (E), r 5 (D), w 1 (C), w 3 (F), r 6 (D), w 5 (E), w 6 (D) Assumendo che ogni transazione sia iniziata dal nodo su cui si trova la prima risorsa acceduta, e che si verifica l’invocazione di una sotto-transazione quando si accede a una risorsa remota, si costruiscano le condizioni di attesa e le si mostri in forma grafica, si indichino gli eventuali messaggi da inviare secondo l’algoritmo di Obermark, e se ne simuli l’esecuzione per rilevare eventuali condizioni di deadlock. 14
α β γ A B C r 3 D r 2 E F r r 1 (E), r 2 (D), r 3 (A), r 2 (C), w 1 (B), r 4 (B), w 4 (A), r 3 (E), r 5 (D), w 1 (C), w 3 (F), r 6 (D), w 5 (E), w 6 (D) 15 2
α β γ A B C r 3 r 2 D r 2 E F r r 1 (E), r 2 (D), r 3 (A), r 2 (C), w 1 (B), r 4 (B), w 4 (A), r 3 (E), r 5 (D), w 1 (C), w 3 (F), r 6 (D), w 5 (E), w 6 (D) 16
α β γ A B C r 3 w 1 r 2 r 4 D r 2 E F r r 1 (E), r 2 (D), r 3 (A), r 2 (C), w 1 (B), r 4 (B), w 4 (A), r 3 (E), r 5 (D), w 1 (C), w 3 (F), r 6 (D), w 5 (E), w 6 (D)
α β γ A B C r 3 w 1 r 2 w 4 r 4 D r 2,5 E F r 1, r 1 (E), r 2 (D), r 3 (A), r 2 (C), w 1 (B), r 4 (B), w 4 (A), r 3 (E), r 5 (D), w 1 (C), w 3 (F), r 6 (D), w 5 (E), w 6 (D)
α β γ A B C r 3 w 1 r 2 w 4 r 4 w 1 D r 2,5 E F r 1,3 w r 1 (E), r 2 (D), r 3 (A), r 2 (C), w 1 (B), r 4 (B), w 4 (A), r 3 (E), r 5 (D), w 1 (C), w 3 (F), r 6 (D), w 5 (E), w 6 (D)
α β γ A B C r 3 w 1 r 2 w 4 r 4 w 1 D r 2,5,6 w 6 E F r 1,3 w 3 w r 1 (E), r 2 (D), r 3 (A), r 2 (C), w 1 (B), r 4 (B), w 4 (A), r 3 (E), r 5 (D), w 1 (C), w 3 (F), r 6 (D), w 5 (E), w 6 (D)
α β γ A B C r 3 w 1 r 2 w 4 r 4 w 1 D r 2,5,6 w 6 E F r 1,3 w 3 w L’unico messaggio inviabile non permette di scoprire nulla – infatti non c’è nulla da scoprire Messaggio: E β T 5 T 1 E α da inviare al nodo α 21
Là, sui monti con XQuery Il DTD soprastante rappresenta una rete di cammini di montagna che collegano rifugi. Nell'elemento Cammino il primo Rifugio rappresenta il punto di partenza, il secondo il punto di arrivo. Ogni elemento non ulteriormente specificato contiene solo PCData. 22
1. Si esprima in XQuery un'interrogazione che estrae il rifugio in cui arrivano più cammini. (3 punti) I rifugi di partenza e arrivo di ogni cammino sono distinti in base alla posizione. let $ranking := { for $rif in distinct-values(//Nome) let $cont := count( //Cammino[./Rifugio[2]/Nome = $rif ] ) order by $cont return } return { $ranking[1] } 23
In alternativa, meno efficiente della precedente (calcola ogni count due volte): for $rif in distinct-values(//Nome) where count( //Cammino[./Rifugio[2]/Nome = $rif ] ) = max( for $r2 in distinct-values(//Nome) return count( //Cammino[./Rifugio[2]/Nome = $r2 ] ) ) return { $rif } o, scritta più razionalmente: let $drn := distinct-values(//Nome) let $max := max( for $r2 in $drn return count( //Cammino[./Rifugio[2]/Nome = $r2 ] ) ) for $rif in $drn where count( //Cammino[./Rifugio[2]/Nome = $rif ] = $max return { $rif } 24
2. Si estraggano in XQuery tutte le coppie di Rifugi (x, y) per cui (a) non esiste un cammino diretto che li collega ma (b) esistono coppie di cammini che, percorsi in sequenza, permettono di andare da x a y, e per cui (c) oltre al cammino composto più rapido esiste anche un’alternativa che richiede meno del 15% di tempo in più (3 punti) for $rA in distinct-values( //Nome ), $rB in distinct-values( //Nome[ text()!=$rA ] ) let $options := for $c1 in //Cammino[./Rifugio[1]/Nome = $rA], $c2 in //Cammino[./Rifugio[2]/Nome = $rB ] where $c1/Rifugio[2]/Nome = $c2/Rifugio[1]/Nome order by $c1/Minuti+$c2/Minuti return {$c1/Nome} {$c2/Nome} {$c1/Minuti+$c2/Minuti} where count( //Cammino[./Rifugio[1]/Nome = $rA and./Rifugio[2]/Nome = $rB ] ) = 0 and count( $options ) >= 2 and $options[1]/Tempo >= 1.15* $options[2]/Tempo return { $options[position()<=2] } 25
3. Definire uno schema a oggetti per rappresentare gli stessi dati ed esprimere la prima interrogazione in OQL. (4 punti) Il modello dei dati non è ordinato, dobbiamo quindi dare due nomi diversi al rifugio di partenza e di arrivo di ogni cammino. Non traduciamo, invece, la radice del documento. Class Rifugio ( Nome: String, Class Cammino( Numero: Integer, Descrizione: String ) From: *Rifugio, To: *Rifugio, Minuti: Integer ) select R.Nome from R in Rifugio where forall X in Rifugio : count( cammini verso R ) >= count( cammini verso X ) cammini verso α := select Y from Y in Cammino where Y.To = α 26
che diventa: select R.Nome from R in Rifugio where forall X in Rifugio : count( select Y from Y in Cammino where Y.To = R ) >= count( select Y from Y in Cammino where Y.To = X ) In alternativa, senza quantificazione (ma esprimendo il massimo come funzione aggregata): select R from R in Rifugio where count( cammini verso R ) = max( select count( cammini verso R2 ) from R2 in Rifugio ) 27
A tale of two Joins Si supponga di avere una tabella S TUDENTE (Matricola,Nome,Cognome,DataNascita,CorsoDiLaurea) che descrive 150K studenti su 10K blocchi, con un'organizzazione primaria hash, calcolata sull'attributo Matricola. Si ha poi una tabella E SAME (Matricola,CodCorso,Data,Voto) con 2M tuple su 8K blocchi con struttura sequenziale entry-sequenced e un indice secondario sull'attributo Matricola, di fanout pari a 200. Si deve eseguire la query di join select * form Studente S join Esame E on S.Matricola = E.Matricola Stimare il costo, in termini di accessi a disco, per eseguire uno hash join in cui la tabella Esame viene ri-organizzata secondo la stessa funzione hash utilizza per la tabella Studente. Si confronti questo costo con il costo richiesto dall'uso di un join nested loop con la tabella Studente come tabella esterna. Si trascurino gli effetti delle collisioni e la presenza del sistema di caching. 28
S TUDENTE ha 15 tuple per blocco e ogni accesso costa una operazione di i/o. E SAME ha 256 tuple per blocco ma non ha una struttura di acceso primaria a hash. Per costruire la struttura hash-based (è l’unico modo per poterla usare!) occorre scandire l’intera tabella e inserire ogni tupla nel “bucket” opportuno. Per applicare la stessa funzione di hash (ad esempio Matricola mod ) dobbiamo anche conservare lo stesso numero di "bucket", che però - helas - saranno meno “pieni” (le tuple occupavano 8K, diventano 10K). Pagheremo quindi 8K accessi in lettura per leggere e ben 4M in scrittura, perché dobbiamo trovare per ogni esame il bucket giusto in cui inserirlo, e tale blocco dev’essere prima letto, poi modificato aggiungendo l’esame nel punto giusto, e infine riscritto. Lo hash-join poi costa 2 * 10K = 20K accessi per leggere i bucket via via associati allo stesso valore della funzione di hash, per un totale di 4M + 28K accessi (è un costo notevole, che però potrebbe essere ripagato col tempo per le successive esecuzioni dello stesso join se si conserva la struttura a hash creata per l’occasione). 29
Calcoliamo ora il costo del nested loop con E SAME come tabella interna (cioè nel caso in cui leggiamo un blocco della tabella S TUDENTE (iterazione esterna) e per ogni studente cerchiamo in E SAME (iterazione interna) tutti gli esami sostenuti da quel particolare studente. Leggiamo un nuovo blocco di S TUDENTE solo dopo aver esaurito tutti gli studenti nel blocco corrente). L’albero ha profondità media pari a circa 3 ed è relativamente sparso (ha spazio per circa 8M valori di chiave). La ricerca degli esami di un dato studente costa quindi 3 accessi ai nodi dell’albero, più 1 accesso ai blocchi della struttura primaria entry-sequenced per recuperare tutti i campi dell’esame (trascuriamo il caso in cui i puntatori agli esami di uno stesso studente siano ripartiti su più di un nodo foglia dell’albero). In totale pagheremo 3 accessi per ogni studente e uno per ogni esame, cioè 3*150K + 2M accessi. Il caricamento di tutti i blocchi di S TUDENTE nell’iterazione esterna costa inoltre 10K accessi, per cui in totale avremo = 2M + 460K accessi. È un costo inferiore a quello di costruirsi la struttura ad-hoc (poco più di metà), il quale però può ancora considerarsi un buon investimento se, conservando la struttura creata, velocizza poi sostanzialmente le esecuzioni dello stesso join (20K contro 2,5M). 30
Calcoliamo infine, per completezza di analisi, benché non fosse richiesto dal testo, il costo del nested loop con E SAME come tabella esterna (che a prima vista sembra l’opzione più sensata, dato che gli accessi random basati su matricola alla struttura primaria a hash dello S TUDENTE costano molto meno di quelli a E SAME basati sull’indice B+ (1 contro 4)). Possiamo scandire tutta la tabella E SAME pagando solo 8K accessi per la sua lettura, sfruttando la struttura entry-sequenced (non avrebbe senso, infatti, scandire in sequenza le foglie dell’indice B+ e accedere ai blocchi primari “uno studente alla volta”, dato che avere gli esami in ordine di matricola non ci aiuta), e dobbiamo poi accedere 2M volte alla struttura hashed, una volta per ogni esame, a recuperare i dati dello studente (più volte lo stesso studente, ogni volta che ci imbattiamo in un suo esame – ma stiamo trascurando gli effetti della cache), per un totale di 2M+8K accessi. Il costo è minore, ma comunque dello stesso ordine di grandezza di quelli precedentemente calcolati. Un ottimizzatore “miope” potrebbe continuare a scegliere questa ultima opzione senza mai “imbarcarsi” nell’impresa di costruire la struttura hash-based, che invece si rivela strategicamente la più conveniente se il join viene ripetuto frequentemente (come è probabile). 31