Esercizio - Laboratorio sul social network - DDL, DML e interrogazioni (laboratorio)
In questa pagina 7
Testo (laboratorio di Basi di Dati, Ing. Informatica UniPD, 2025-26). Si dà lo schema ER di un piccolo social network (ER.pdf). Cosa si fa:
- trasformare lo schema ER nello schema relazionale;
- creare lo schema in PostgreSQL (DDL);
- popolare lo schema con i dati forniti in
csv/(DML); - scrivere ed eseguire le interrogazioni di
query.sql(27 query numerate da 1 a 28, con le sottovarianti 6.a/6.b e 9.a/9.b e i numeri 17, 25 e 26 assenti dal file).
In alternativa, si può ripristinare il database corretto (/backup, dump socialDB) e lavorare solo sulle query. Questo esercizio svolge tutti e quattro i punti: la traduzione e il DDL, l'inserimento dei dati, e le query con il risultato su quei dati. Teoria: SQL - definizione dei dati e vincoliSQL (standard ISO 9075) è un linguaggio dichiarativo con una parte di definizione dei dati (DDL: CREATE/DROP/ALTER di database, schemi, tipi enumerati, domini, tabelle) e una di manipolazione (DML: INSERT, DELETE, UPDATE, SELECT). Una tabella SQL è un multi-insieme di righe (può avere duplicati), quindi non è una relazione in senso stretto. CREATE TABLE elenca colonne con tipo e vincoli: NOT NULL, DEFAULT, CHECK (di colonna o di tupla), PRIMARY KEY (una sola, anche composta), UNIQUE, FOREIGN KEY ... REFERENCES con ON DELETE/ON UPDATE (NO ACTION, CASCADE, SET NULL, SET DEFAULT). I tipi principali sono CHAR/VARCHAR, NUMERIC, INTEGER, SERIAL, BOOLEAN, DATE, TIME, TIMESTAMP, più UUID, intervalli e JSON in PostgreSQL. DROP ... CASCADE cancella anche gli oggetti dipendenti, RESTRICT (predefinito) rifiuta se ce ne sono. DELETE e UPDATE senza WHERE agiscono su tutta la tabella.SQL - definizione dei dati e vincoli →, 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 →; la traduzione ER relazionale: 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 →.
I comandi sono quelli di PostgreSQL; le query sono state eseguite su SQLite con gli stessi dati (le differenze sono segnalate: tipi ENUM, SERIAL).
Progettazione ER e schema relazionale della stessa traccia: Esercizio - Progettazione ER e schema relazionale del social network (esercizio social e laboratorio).
1. Lo schema ER, in parole
| Entità | Attributi | Identificatore |
|---|---|---|
UTENTE |
Email, Username, Password, Subscription_Date | |
POST |
ID, Testo, Visibilita | ID (seriale) |
COMMENTO |
ID, Testo | ID (seriale) |
| Relazione | Entità e cardinalità | Attributi |
|---|---|---|
| segue (nel disegno ha l'etichetta "Sceglie") | UTENTE come Follower – UTENTE come Followed (ricorsiva) |
|
| pubblica (nel disegno "Sceglie") | UTENTE – POST |
Data_Ora |
| scrive (ternaria) | UTENTE – POST – COMMENTO |
Data_Ora |
risponde (ricorsiva su COMMENTO) |
COMMENTO come Commento_Origine – COMMENTO come Commento_Risposta |
Un utente può avere zero o più post (ogni post ha esattamente un autore); un commento è scritto da un utente su un post (esattamente uno per ciascuno); un commento può essere risposta a un altro commento (al più uno), che può avere più risposte. Il disegno etichetta con lo stesso nome ("Sceglie") due relazioni diverse (segue e pubblica): è un'incongruenza del PDF, la semantica è quella della tabella.
2. Traduzione nel modello relazionale
UTENTE(Email, Username, Password, Subscription_Date):Usernameè una chiave alternativa (UNIQUE);POST(ID, Testo, Data_Ora, Utente, Visibilita): la relazione pubblica – diventa la chiave esternaUtenteUTENTEdalla parte del , con l'attributoData_Oradella relazione;COMMENTO(ID, Testo, Data_Ora, Commento_Origine, Post, Utente): la relazione ternaria scrive, avendoCOMMENTOcon cardinalità , si riduce a due chiavi esternePostPOSTeUtenteUTENTE(e portaData_Ora); risponde dalla parte della risposta diventa la chiave esterna nullableCommento_OrigineCOMMENTO(autoriferimento): NULL per un commento che non è una risposta;SEGUE(Follower,Followed): relazione molti-a-molti ricorsiva: tabella con le due chiavi esterne versoUTENTEcome chiave composta.
Vincoli esterni: un utente non segue se stesso (CHECK); la risposta deve riferirsi a un commento dello stesso post (non esprimibile con una chiave esterna semplice: trigger o controllo applicativo).
3. DDL (PostgreSQL)
CREATE TYPE visibility AS ENUM ('public', 'private', 'friends');
CREATE TABLE Utente (
Email VARCHAR(255) PRIMARY KEY,
Username VARCHAR(50) NOT NULL UNIQUE,
Password VARCHAR(50) NOT NULL,
Subscription_Date DATE NOT NULL
);
CREATE TABLE Post (
ID SERIAL PRIMARY KEY,
Testo TEXT NOT NULL,
Data_Ora TIMESTAMP WITH TIME ZONE NOT NULL,
Utente VARCHAR(255) NOT NULL REFERENCES Utente(Email) ON DELETE CASCADE,
Visibilita visibility NOT NULL DEFAULT 'public'
);
CREATE TABLE Commento (
ID SERIAL PRIMARY KEY,
Testo TEXT NOT NULL,
Data_Ora TIMESTAMP NOT NULL,
Commento_Origine INTEGER REFERENCES Commento(ID) ON DELETE CASCADE,
Post INTEGER NOT NULL REFERENCES Post(ID) ON DELETE CASCADE,
Utente VARCHAR(255) NOT NULL REFERENCES Utente(Email) ON DELETE CASCADE
);
CREATE TABLE Segue (
Follower VARCHAR(255) REFERENCES Utente(Email) ON UPDATE CASCADE ON DELETE CASCADE,
Followed VARCHAR(255) REFERENCES Utente(Email) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (Follower, Followed),
CHECK (Follower <> Followed)
);Scelte da giustificare: Email come chiave naturale (le chiavi esterne la ripetono: ON UPDATE CASCADE su Segue permette di cambiarla); ID SERIAL per post e commenti (identificatori senza significato, non c'è nulla di naturale che li identifichi); ENUM per la visibilità con default 'public'; ON DELETE CASCADE: cancellando un utente spariscono i suoi post, commenti e legami Segue; cancellando un post i suoi commenti; cancellando un commento le sue risposte. Il dump del corso usa TIMESTAMP senza fuso per Commento.Data_Ora e con fuso per Post.Data_Ora: una differenza che conviene eliminare (tutto con fuso).
Per SQLite (usato per le prove) si è tradotto SERIAL in INTEGER PRIMARY KEY AUTOINCREMENT, visibility in TEXT CHECK (Visibilita IN ('public','private','friends')), e si è attivato PRAGMA foreign_keys = ON.
4. DML: i dati
I file csv/utente.csv, post.csv, commento.csv, segue.csv contengono 5 utenti, 5 post, 6 commenti e 6 legami Segue. In PostgreSQL si caricano con \copy o COPY ... FROM '...' WITH (FORMAT csv, HEADER true, NULL 'NULL'); l'ordine rispetta le chiavi esterne: prima Utente, poi Post e Segue, infine Commento. A mano:
INSERT INTO Utente VALUES ('alice@example.com','alice_w','passalice','2024-01-10'),
('bob@example.com','bobby12','passbob','2024-02-15'),
('carol@example.com','carol89','passcarol','2024-03-20'),
('dave@example.com','daveTech','passdave','2024-04-05'),
('eva@example.com','eva_moon','passeva','2024-05-01');
INSERT INTO Post(ID, Testo, Data_Ora, Utente, Visibilita) VALUES
(1,'Ciao a tutti! Questo è il mio primo post!','2024-05-10 11:15:00+02','alice@example.com','public'),
(2,'Giornata intensa di lavoro, ma soddisfacente.','2024-05-11 19:40:00+02','bob@example.com','friends'),
(3,'Che bella giornata di sole oggi!','2024-05-12 10:00:00+02','carol@example.com','public'),
(4,'Sto lavorando a un nuovo progetto interessante.','2024-05-13 15:20:00+02','dave@example.com','private'),
(5,'Qualcuno vuole uscire a fare una passeggiata?','2024-05-14 17:00:00+02','eva@example.com','public');
INSERT INTO Commento(ID, Testo, Data_Ora, Commento_Origine, Post, Utente) VALUES
(1,'Bel post Alice!','2024-05-10 11:00:00',NULL,1,'bob@example.com'),
(2,'Sono daccordo!','2024-05-12 09:30:00',NULL,3,'alice@example.com'),
(3,'Interessantissimo!','2024-05-13 15:00:00',NULL,4,'carol@example.com'),
(4,'Grazie Bob!','2024-05-10 11:10:00',1,1,'alice@example.com'),
(5,'Vero, giornata splendida!','2024-05-12 09:45:00',2,3,'dave@example.com'),
(6,'Raccontaci di più!','2024-05-13 15:30:00',3,4,'eva@example.com');
INSERT INTO Segue VALUES ('alice@example.com','bob@example.com'),('alice@example.com','carol@example.com'),
('bob@example.com','alice@example.com'),('carol@example.com','dave@example.com'),
('dave@example.com','eva@example.com'),('eva@example.com','alice@example.com');Se gli ID si inseriscono a mano, la sequenza SERIAL non avanza: dopo il caricamento va riallineata (SELECT setval('post_id_seq', 5); SELECT setval('commento_id_seq', 6);, come fa il dump), altrimenti il primo INSERT senza ID produce una chiave duplicata. L'ordine dei commenti rispetta anche l'autoriferimento: una risposta (ID 4, 5, 6) si inserisce dopo il commento a cui risponde. I commenti 1 e 4 (11:00 e 11:10) sono datati prima del post 1 (11:15): un'incoerenza dei dati del laboratorio (un commento prima del suo post), irrilevante per le query ma non per un controllo di integrità.
Riassunto dei dati: post: 1 di alice (pubblico), 2 di bob (friends), 3 di carol (pubblico), 4 di dave (privato), 5 di eva (pubblico). Commenti: 1 (bob su post 1), 2 (alice su 3), 3 (carol su 4), 4 (alice su 1, risposta al 1), 5 (dave su 3, risposta al 2), 6 (eva su 4, risposta al 3). Segue (follower followed): alice bob, carol; bob alice; carol dave; dave eva; eva alice. Nei risultati le email sono abbreviate al nome (alice = alice@example.com).
5. Le interrogazioni
Il file query.sql contiene già le soluzioni; qui si ripetono con il risultato sui dati del laboratorio. Dove il risultato sarebbe vuoto (non istruttivo) si indica anche l'effetto su dati aggiuntivi inventati: un utente frank (non segue e non è seguito), un secondo post di alice (visibilità friends) e tre commenti di dave, uno al post 1 e due al post 5.
1. Email e username degli utenti, per data di iscrizione. SELECT Email, Username FROM Utente ORDER BY Subscription_Date; alice_w, bobby12, carol89, daveTech, eva_moon. Errore in query.sql: il file inizia con select * from segue senza il punto e virgola, seguito subito dalla query 1: il blocco diventa select * from segue SELECT Email, ..., errore di sintassi (provato). Va tolta la riga o chiusa con ;.
2. I post pubblici. SELECT * FROM Post WHERE Visibilita = 'public'; i post 1, 3, 5.
3. Numero di post di ogni utente. SELECT Utente, COUNT(*) AS Numero_Post FROM Post GROUP BY Utente; ogni utente 1. Errore nella traccia del laboratorio: l'intestazione dice "(attenzione sono tutti 2)", ma con i dati forniti ogni utente ha un solo post (il commento è un residuo di una versione precedente dei dati).
4. I follower di Alice. SELECT Follower FROM Segue WHERE Followed = 'alice@example.com'; bob, eva. 5. Gli utenti seguiti da Bob. SELECT Followed FROM Segue WHERE Follower = 'bob@example.com'; alice. (Selezione su Segue: la direzione è nell'ordine delle colonne.)
6.a Commenti che non sono una risposta. SELECT * FROM Commento WHERE Commento_Origine IS NULL; 1, 2, 3. (IS NULL, non = NULL.)
6.b Commenti che non sono una risposta e non hanno ricevuto risposte.
SELECT id FROM Commento WHERE Commento_Origine IS NULL
EXCEPT
SELECT Commento_Origine FROM Commento WHERE Commento_Origine IS NOT NULL;Il primo insieme è , il secondo (i commenti che hanno ricevuto risposta) : la differenza è vuota sui dati. Con tre nuovi commenti radice senza risposta (ID 7, 8, 9) darebbe 7, 8, 9. IS NOT NULL nella seconda parte è prudente: una EXCEPT con NULL nei valori funziona (i NULL si considerano uguali negli operatori insiemistici), ma l'intenzione è più chiara.
7. Ogni post con il creatore. SELECT Post.ID, Post.Testo, Utente.Username FROM Post INNER JOIN Utente ON Post.Utente = Utente.Email; 5 righe, una per post (alice_w, bobby12, carol89, daveTech, eva_moon).
8. Commenti per post. SELECT Post, COUNT(*) AS Numero_Commenti FROM Commento GROUP BY Post; post 1: 2, post 3: 2, post 4: 2. I post 2 e 5 (senza commenti) non compaiono: per averli con 0 si usa Post p LEFT JOIN Commento c ON c.Post = p.ID e COUNT(c.ID).
9.a Commenti con il testo del post. SELECT c.Testo, p.Testo AS Post_Testo FROM Commento AS c INNER JOIN Post AS p ON c.Post = p.ID; 6 righe (una per commento).
9.b Commenti con il commento a cui rispondono (self-join).
SELECT c.Testo AS Commento_testo, r.Testo AS Risposta_Testo
FROM Commento AS c INNER JOIN Commento AS r ON c.id = r.Commento_Origine; 3 righe: (Bel post Alice!, Grazie Bob!), (Sono daccordo!, Vero, giornata splendida!), (Interessantissimo!, Raccontaci di più!). Nota sui nomi: nella query c è il commento originale e r la risposta; la traccia chiede "il testo dei commenti insieme al testo del commento a cui rispondono", cioè per ogni risposta il suo originale: le due colonne sono nell'ordine (originale, risposta), l'opposto della frase. Il contenuto è lo stesso, ma le etichette (Commento_testo, Risposta_Testo) vanno lette con questo ordine.
10. Utenti con almeno 2 post. SELECT Utente FROM Post GROUP BY Utente HAVING COUNT(*) >= 2; vuoto sui dati (un post a testa). Con il secondo post di alice: alice. 11. Come la 10, con il numero di post, in ordine decrescente. ... HAVING COUNT(*) >= 2 ORDER BY Numero_Post DESC; vuoto; con il dato aggiuntivo: (alice, 2).
12. Utenti e numero di follower. SELECT Followed AS Utente, COUNT(*) AS Numero_Follower FROM Segue GROUP BY Followed; alice 2, bob 1, carol 1, dave 1, eva 1. Chi non ha follower (frank) non compare: con Utente u LEFT JOIN Segue s ON s.Followed = u.Email e COUNT(s.Follower) comparirebbe con 0.
13. Post con almeno un commento. SELECT DISTINCT p.ID, p.Testo FROM Post p INNER JOIN Commento c ON p.ID = c.Post; 1, 3, 4. Il DISTINCT serve: il join ripete il post una volta per commento (2 commenti per post).
14. Utenti che non seguono nessuno. SELECT Email FROM Utente EXCEPT SELECT Follower FROM Segue; vuoto (tutti seguono qualcuno); con frank: frank. In algebra: (provato). 15. Utenti non seguiti da nessuno. Stessa forma con Followed: vuoto sui dati, frank con il dato aggiuntivo.
16. Commenti con almeno una risposta. SELECT DISTINCT c1.ID, c1.Testo FROM Commento AS c1 INNER JOIN Commento AS c2 ON c2.Commento_Origine = c1.ID; 1, 2, 3 (self-join).
18. Ogni post con il numero di commenti di primo livello.
SELECT p.ID, COUNT(c.ID) AS Commenti_Radice
FROM Post AS p LEFT JOIN Commento AS c ON p.ID = c.Post AND c.Commento_Origine IS NULL
GROUP BY p.ID; post 1: 1, 2: 0, 3: 1, 4: 1, 5: 0. Il filtro "è un commento radice" è nella ON e non nella WHERE, come si deve per un join esterno: LEFT JOIN conserva i post anche senza commenti di primo livello, e COUNT(c.ID) (non COUNT(*)) dà 0 per loro. Con il filtro in WHERE il risultato è uguale solo per caso qui (la condizione IS NULL è vera anche per le righe a destra senza corrispondenza); la forma con ON è sempre corretta. Con i dati aggiuntivi: il post 5 passa a 2.
19. Utenti che hanno commentato almeno 2 post diversi. SELECT Utente FROM Commento GROUP BY Utente HAVING COUNT(DISTINCT Post) >= 2; alice (post 3 e post 1). Il DISTINCT evita di contare due commenti sullo stesso post.
20. Post che hanno solo commenti di un unico utente. SELECT p.ID FROM Post p INNER JOIN Commento c ON p.ID = c.Post GROUP BY p.ID HAVING COUNT(DISTINCT c.Utente) = 1; vuoto sui dati (ogni post è commentato da due utenti); con i dati aggiuntivi: il post 5 (solo dave).
21. Utenti che hanno commentato post altrui. SELECT DISTINCT c.Utente FROM Commento AS c INNER JOIN Post p ON c.Post = p.ID WHERE c.Utente <> p.Utente; bob, alice, carol, dave, eva (tutti, almeno una volta).
22. I post con il numero massimo di commenti.
SELECT p.ID, COUNT(c.ID) AS Numero_Commenti
FROM Post p INNER JOIN Commento c ON p.ID = c.Post
GROUP BY p.ID
HAVING COUNT(c.ID) = (SELECT MAX(Count_Post)
FROM (SELECT COUNT(*) AS Count_Post FROM Commento GROUP BY Post) AS tmp); 1, 3 e 4, tutti con 2 (pari merito). Con i dati aggiuntivi vince il post 1 (3 commenti: la query restituisce solo lui). La variante con vista (Post_Comment_Count, con LEFT JOIN) dà lo stesso risultato; la differenza è che la vista elenca anche i post a 0 commenti, rilevante solo se il massimo fosse 0. Vedi 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 →.
23. Utenti con il massimo numero di follower. SELECT Followed AS Utente FROM Segue GROUP BY Followed HAVING COUNT(*) = (SELECT MAX(Num) FROM (SELECT COUNT(*) AS Num FROM Segue GROUP BY Followed) AS tmp); alice (2 follower).
24. Utenti che hanno commentato almeno 3 post pubblici diversi. SELECT c.Utente FROM Commento AS c INNER JOIN Post p ON c.Post = p.ID WHERE p.Visibilita = 'public' GROUP BY c.Utente HAVING COUNT(DISTINCT p.ID) >= 3; vuoto; con i dati aggiuntivi: dave (post 3, 1, 5, tutti pubblici). Il filtro sui post pubblici è in WHERE, prima del raggruppamento.
27. Utenti che hanno commentato post di almeno 2 utenti diversi. ... WHERE c.Utente <> p.Utente GROUP BY c.Utente HAVING COUNT(DISTINCT p.Utente) >= 2; vuoto sui dati; con i dati aggiuntivi: dave (alice, carol, eva).
28. Utenti che hanno scritto più commenti su post altrui che sui propri.
SELECT u.Email
FROM Utente AS u
WHERE (SELECT COUNT(*) FROM Commento c JOIN Post p ON c.Post = p.ID
WHERE c.Utente = u.Email AND p.Utente <> u.Email)
> (SELECT COUNT(*) FROM Commento c JOIN Post p ON c.Post = p.ID
WHERE c.Utente = u.Email AND p.Utente = u.Email); bob, carol, dave, eva (alice ha 1 commento sul post altrui e 1 sul proprio: è falso). Sono due sottoquery correlate su u.Email, ciascuna restituisce un solo conteggio; si confrontano due scalari.
Errori nel materiale del laboratorio (riassunto)
query.sql: la rigaselect * from seguesenza;rende sintatticamente errata la query 1.- Traccia della query 3: "(attenzione sono tutti 2)", ma i dati forniti hanno un solo post per utente; di conseguenza le query 10, 11, 14, 15, 20, 24, 27 danno risultato vuoto sui dati del laboratorio (utili solo con dati aggiuntivi).
- La numerazione salta 17, 25 e 26.
ER.pdf: due relazioni diverse con la stessa etichetta "Sceglie".- Dati: i commenti 1 e 4 sul post 1 sono datati prima del post (11:00 e 11:10 contro 11:15);
Post.Data_Oraha fuso orario,Commento.Data_Orano. - Query 9.b: l'ordine delle colonne (originale, risposta) è l'opposto di quello della frase.
Versione ripasso
Teoria: SQL - definizione dei dati e vincoliSQL (standard ISO 9075) è un linguaggio dichiarativo con una parte di definizione dei dati (DDL: CREATE/DROP/ALTER di database, schemi, tipi enumerati, domini, tabelle) e una di manipolazione (DML: INSERT, DELETE, UPDATE, SELECT). Una tabella SQL è un multi-insieme di righe (può avere duplicati), quindi non è una relazione in senso stretto. CREATE TABLE elenca colonne con tipo e vincoli: NOT NULL, DEFAULT, CHECK (di colonna o di tupla), PRIMARY KEY (una sola, anche composta), UNIQUE, FOREIGN KEY ... REFERENCES con ON DELETE/ON UPDATE (NO ACTION, CASCADE, SET NULL, SET DEFAULT). I tipi principali sono CHAR/VARCHAR, NUMERIC, INTEGER, SERIAL, BOOLEAN, DATE, TIME, TIMESTAMP, più UUID, intervalli e JSON in PostgreSQL. DROP ... CASCADE cancella anche gli oggetti dipendenti, RESTRICT (predefinito) rifiuta se ce ne sono. DELETE e UPDATE senza WHERE agiscono su tutta la tabella.SQL - definizione dei dati e vincoli →, 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 →. Traduzione ER: 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 →.
Schema ER. UTENTE(Email, Username, Password, Subscription_Date); POST(ID seriale, Testo, Visibilita); COMMENTO(ID seriale, Testo). Relazioni: segue ricorsiva su UTENTE (N-N); pubblica tra utente e post , con Data_Ora; scrive ternaria utente-post-commento, con il commento ; risponde ricorsiva su COMMENTO, con la risposta .
Schema relazionale
Utente(Email, Username, Password, Subscription_Date), conUsernameUNIQUE.Post(ID, Testo, Data_Ora, Utente, Visibilita), conUtentechiave esterna versoUtente(Email).Commento(ID, Testo, Data_Ora, Commento_Origine, Post, Utente):Commento_Origineè nullable e punta a un altro commento (autoriferimento);PosteUtentesono NOT NULL.Segue(Follower, Followed), con chiave composta eCHECK (Follower <> Followed).
CREATE TABLE Segue (
Follower VARCHAR(255) REFERENCES Utente(Email) ON UPDATE CASCADE ON DELETE CASCADE,
Followed VARCHAR(255) REFERENCES Utente(Email) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (Follower, Followed),
CHECK (Follower <> Followed)
);- Scelte:
Emailcome chiave naturale;SERIALper post e commenti;ENUMper la visibilità con default'public';ON DELETE CASCADEsu utente, post e commento. - SQLite:
TEXT CHECK (Visibilita IN (...)),AUTOINCREMENT,PRAGMA foreign_keys = ON.
DML. Si carica prima Utente, poi Post e Segue, infine Commento, per rispettare le chiavi esterne. Se gli ID si inseriscono a mano, la sequenza va riallineata: SELECT setval('post_id_seq', 5);. Una risposta si inserisce dopo il commento a cui risponde. I dati sono 5 utenti, 5 post, 6 commenti e 6 legami Segue. Incoerenza: i commenti 1 e 4 sono datati prima del post 1.
Interrogazioni, con il risultato sui dati del laboratorio
- 1. Utenti per data di iscrizione: alice_w, bobby12, carol89, daveTech, eva_moon. Il file
query.sqlinizia conselect * from seguesenza;: errore di sintassi. - 2. Post pubblici: 1, 3, 5. 3. Post per utente: 1 ciascuno. La traccia dice "sono tutti 2": è falso.
- 4-5. Follower di alice: bob, eva. Seguiti da bob: alice.
- 6.a Commenti radice: 1, 2, 3. 6.b Radici senza risposta, con
EXCEPT: vuoto. - 8. Commenti per post: post 1, 3 e 4 con 2 ciascuno. I post 2 e 5 non compaiono; per averli con 0 si usa
LEFT JOINconCOUNT(c.ID).
SELECT c.Testo AS Commento_testo, r.Testo AS Risposta_Testo
FROM Commento AS c INNER JOIN Commento AS r ON c.id = r.Commento_Origine;- 9.b Self-join: tre coppie (originale, risposta). L'ordine delle colonne è l'opposto di quello della traccia.
- 12. Follower: alice 2, bob 1, carol 1, dave 1, eva 1.
- 13. Post con commenti, con
DISTINCT: 1, 3, 4. 14. Utenti che non seguono nessuno: vuoto. - 16. Commenti con risposta: 1, 2, 3.
- 18. Commenti di primo livello per post: post 1: 1, post 2: 0, post 3: 1, post 4: 1, post 5: 0.
SELECT p.ID, COUNT(c.ID) AS Commenti_Radice
FROM Post AS p LEFT JOIN Commento AS c ON p.ID = c.Post AND c.Commento_Origine IS NULL
GROUP BY p.ID;Il filtro sul commento va nella
ON: nellaWHEREsi perderebbero i post senza commenti.19. Utenti che hanno commentato almeno 2 post diversi: alice.
22. Post con il massimo numero di commenti: 1, 3 e 4, pari merito con 2. Il massimo è una sottoquery sul conteggio per post, scritta come tabella derivata:
SELECT p.ID, COUNT(c.ID) AS Numero_Commenti
FROM Post p INNER JOIN Commento c ON p.ID = c.Post
GROUP BY p.ID
HAVING COUNT(c.ID) = (SELECT MAX(Count_Post)
FROM (SELECT COUNT(*) AS Count_Post FROM Commento GROUP BY Post) AS tmp);- 23. Utente con più follower: alice (2).
- 28. Utenti con più commenti su post altrui che sui propri: bob, carol, dave, eva.
Dati aggiuntivi (utente frank, un secondo post di alice, tre commenti di dave): 10 e 11 danno alice; 14 e 15 danno frank; 20 dà il post 5; 24 e 27 danno dave.
Errori nel materiale
query.sql:select * from seguesenza;rompe la query 1.- La traccia dice che ogni utente ha due post, ma i dati ne hanno uno a testa; le query 10, 11, 14, 15, 20, 24 e 27 danno vuoto senza i dati aggiuntivi.
- La numerazione salta 17, 25 e 26.
- Il PDF usa la stessa etichetta "Sceglie" per due relazioni diverse.
Errori tipici
COUNT(*)su un left join.- Filtro della tabella di destra in
WHERE. = NULLinvece diIS NULL.DISTINCTdimenticato dopo un join a molti.- Chiavi esterne inserite prima della tabella referenziata.