Esercizio - Triple RDF in Turtle e query SPARQL sulle aule (applicazione web)
Esercizio originale, nello stile delle domande sul web semantico. Teoria: Web semantico - RDF, linked open data e SPARQLIl web semantico (Web 3.0) dà ai dati un significato leggibile dalle macchine. RDF 1.1 rappresenta ogni fatto come tripla soggetto-predicato-oggetto; soggetti e predicati sono risorse identificate da IRI, gli oggetti sono IRI o letterali (testo con lingua, valori tipizzati xsd:integer, xsd:date), più nodi anonimi; un insieme di triple è un grafo orientato etichettato. Turtle è la sintassi testuale: @prefix per abbreviare gli IRI, a per rdf:type, il punto e virgola per ripetere il soggetto, la virgola per ripetere soggetto e predicato. Un vocabolario dichiara classi e proprietà: RDFS (rdfs:Class, rdfs:subClassOf, rdfs:domain, rdfs:range, rdfs:label) permette inferenze, OWL aggiunge l'espressività dell'ontologia (owl:sameAs per dire che due IRI indicano la stessa cosa, equivalenze, cardinalità). I linked open data seguono le quattro regole di Berners-Lee: usare gli URI come nomi delle cose, usare URI HTTP che si possano consultare, restituire informazioni utili in standard (RDF, SPARQL), includere link ad altri URI; con licenza aperta e collegamenti arrivano alle cinque stelle. SPARQL 1.1 interroga i grafi: SELECT con pattern di triple e variabili (?nome), FILTER, OPTIONAL, UNION, FILTER NOT EXISTS, ORDER BY, LIMIT, GROUP BY con COUNT e HAVING, ASK (risposta sì o no), CONSTRUCT. Rispetto al modello relazionale gli identificatori sono globali, lo schema è flessibile e vale l'ipotesi del mondo aperto.Web semantico - RDF, linked open data e SPARQL →; per il confronto con il relazionale: Modello relazionale - relazioni, chiavi e vincoliIl modello relazionale (Codd, IBM, 1970) rappresenta i dati con relazioni, cioè tabelle, ed è basato su valori: i legami tra relazioni sono confronti di valori, non puntatori, il che garantisce indipendenza dalle strutture fisiche, portabilità e bidirezionalità. Una relazione matematica è un sottoinsieme del prodotto cartesiano di n domini (insieme di n-uple ordinate, senza duplicati né ordine tra le righe); una relazione del modello è un insieme di tuple, funzioni dagli attributi ai valori del dominio, quindi non posizionale. Si distinguono schema di relazione e istanza, schema di base di dati (relazioni più vincoli) e istanza. NULL è un valore speciale fuori da ogni dominio. I vincoli di integrità sono intra-relazionali (di dominio, di tupla, di chiave) e inter-relazionali (integrità referenziale, con la chiave esterna). Superchiave: insieme di attributi che non ripete mai i valori; chiave: superchiave minimale; ogni relazione ne ha almeno una (tutti gli attributi); la chiave primaria è una chiave scelta, senza NULL.Modello relazionale - relazioni, chiavi 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 →. Il file Turtle è stato letto da un piccolo analizzatore scritto per la prova (prefissi, punti, punti e virgola, letterali, conteggio delle triple: 55); non c'era un motore SPARQL, quindi i risultati delle query sono stati ottenuti valutando con un programma gli stessi criteri sul grafo (stesse condizioni, stessi risultati attesi di un motore).
Testo
Il database dell'applicazione di prenotazione delle aule contiene:
| aula.id | nome | posti |
|---|---|---|
| 1 | Aula A1 | 80 |
| 2 | Aula B2 | 45 |
| 3 | Lab Info | 30 |
| prenotazione.id | aula_id | giorno | ora_inizio | ora_fine | studente |
|---|---|---|---|---|---|
| 1 | 1 | 2026-10-19 | 09:00 | 11:00 | Rossi |
| 2 | 1 | 2026-10-19 | 14:00 | 16:00 | Bianchi |
| 3 | 2 | 2026-10-20 | 10:00 | 12:00 | Verdi |
| 4 | 3 | 2026-10-21 | 09:00 | 13:00 | Rossi |
- Scrivere in Turtle il vocabolario (classi
AulaePrenotazione, proprietà condomainerange) e i dati, con prefissoex:=http://example.org/aule/. Dire quante triple risultano e disegnare il grafo di una prenotazione. - Scrivere in SPARQL:
- (a) nome e posti delle aule con almeno 45 posti, dalla più grande;
- (b) i nomi delle aule senza prenotazioni il 19 ottobre 2026;
- (c) gli studenti con più di una prenotazione;
- (d) una query
ASK: l'Aula A1 è libera il 19 ottobre dalle 10:00 alle 12:00?
- Quali triple si deducono con RDFS da
ex:inAula rdfs:domain ex:Prenotazionese si aggiungeex:pren5 ex:inAula ex:labInfo? Che cosa diceowl:sameAse come si collegherebbe l'ateneo a DBpedia? - Confrontare la query (b) con l'equivalente SQL.
Soluzione
1. Il dataset in Turtle
@prefix ex: <http://example.org/aule/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# --- vocabolario (RDFS): classi e proprietà ---
ex:Aula a rdfs:Class ; rdfs:label "Aula"@it .
ex:Prenotazione a rdfs:Class ; rdfs:label "Prenotazione"@it .
ex:posti a rdf:Property ; rdfs:domain ex:Aula ; rdfs:range xsd:integer .
ex:inAula a rdf:Property ; rdfs:domain ex:Prenotazione ; rdfs:range ex:Aula .
ex:giorno a rdf:Property ; rdfs:domain ex:Prenotazione ; rdfs:range xsd:date .
ex:oraInizio a rdf:Property ; rdfs:domain ex:Prenotazione ; rdfs:range xsd:time .
ex:oraFine a rdf:Property ; rdfs:domain ex:Prenotazione ; rdfs:range xsd:time .
ex:studente a rdf:Property ; rdfs:domain ex:Prenotazione ; rdfs:range xsd:string .
# --- dati: le tre aule e le quattro prenotazioni dell'esempio ---
ex:aulaA1 a ex:Aula ; rdfs:label "Aula A1"@it ; ex:posti 80 .
ex:aulaB2 a ex:Aula ; rdfs:label "Aula B2"@it ; ex:posti 45 .
ex:labInfo a ex:Aula ; rdfs:label "Lab Info"@it ; ex:posti 30 .
ex:pren1 a ex:Prenotazione ; ex:inAula ex:aulaA1 ; ex:giorno "2026-10-19"^^xsd:date ;
ex:oraInizio "09:00:00"^^xsd:time ; ex:oraFine "11:00:00"^^xsd:time ; ex:studente "Rossi" .
ex:pren2 a ex:Prenotazione ; ex:inAula ex:aulaA1 ; ex:giorno "2026-10-19"^^xsd:date ;
ex:oraInizio "14:00:00"^^xsd:time ; ex:oraFine "16:00:00"^^xsd:time ; ex:studente "Bianchi" .
ex:pren3 a ex:Prenotazione ; ex:inAula ex:aulaB2 ; ex:giorno "2026-10-20"^^xsd:date ;
ex:oraInizio "10:00:00"^^xsd:time ; ex:oraFine "12:00:00"^^xsd:time ; ex:studente "Verdi" .
ex:pren4 a ex:Prenotazione ; ex:inAula ex:labInfo ; ex:giorno "2026-10-21"^^xsd:date ;
ex:oraInizio "09:00:00"^^xsd:time ; ex:oraFine "13:00:00"^^xsd:time ; ex:studente "Rossi" .- Le chiavi diventano IRI. La riga
aula.id = 1diventa il soggettoex:aulaA1, eprenotazione.id = 1diventaex:pren1. La chiave esternaaula_idnon è più un numero da confrontare con un join, ma un arcoex:inAulail cui oggetto è l'IRI dell'aula (ex:pren1 ex:inAula ex:aulaA1). a=rdf:type.;ripete il soggetto. I numeri senza virgolette (80) sonoxsd:integer. Giorno e ore sono letterali tipizzati (xsd:date,xsd:time); il nome dell'aula è un'etichettardfs:labelcon lingua.- Conteggio. Il vocabolario ha 2 classi 2 triple (tipo ed etichetta) e 6 proprietà 3 triple (tipo,
domain,range) ; le 3 aule hanno 3 triple ciascuna (tipo, etichetta, posti); le 4 prenotazioni hanno 6 triple ciascuna (tipo, aula, giorno, inizio, fine, studente). Totale triple. - Grafo di
ex:pren1:
ex:pren1 ──a──────────> ex:Prenotazione
├─inAula─────> ex:aulaA1 ──a────> ex:Aula
│ ├─label─> "Aula A1"@it
│ └─posti─> 80
├─giorno─────> "2026-10-19"^^xsd:date
├─oraInizio──> "09:00:00"^^xsd:time
├─oraFine────> "11:00:00"^^xsd:time
└─studente───> "Rossi"Se uno studente non avesse l'ora di fine, la tripla ex:oraFine non esisterebbe: in RDF non c'è un NULL.
2. Le query SPARQL
Prefissi comuni:
PREFIX ex: <http://example.org/aule/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>(a) Aule con almeno 45 posti.
SELECT ?nome ?posti
WHERE {
?aula a ex:Aula ;
rdfs:label ?nome ;
ex:posti ?posti .
FILTER (?posti >= 45)
}
ORDER BY DESC(?posti)La variabile ?aula collega le tre triple (equivale a una riga di aula, ed è l'unione delle condizioni, come un join); FILTER seleziona. Risultato:
| nome | posti |
|---|---|
| "Aula A1"@it | 80 |
| "Aula B2"@it | 45 |
(b) Aule senza prenotazioni il 19 ottobre.
SELECT ?nome
WHERE {
?aula a ex:Aula ; rdfs:label ?nome .
FILTER NOT EXISTS { ?p ex:inAula ?aula ; ex:giorno "2026-10-19"^^xsd:date }
}FILTER NOT EXISTS è la negazione: tiene le aule per cui non esiste una prenotazione ?p in quell'aula con quel giorno. Risultato: Aula B2 (la sua prenotazione è il 20) e Lab Info (il 21). Aula A1 ha due prenotazioni il 19 e viene esclusa.
(c) Studenti con più di una prenotazione.
SELECT ?studente (COUNT(?p) AS ?n)
WHERE { ?p a ex:Prenotazione ; ex:studente ?studente }
GROUP BY ?studente
HAVING (COUNT(?p) > 1)Si raggruppa per studente e si conta, come in SQL. Risultato: Rossi, 2 (prenotazioni 1 e 4); Bianchi e Verdi hanno una prenotazione.
(d) A1 è libera il 19 ottobre 10:00-12:00?
ASK {
?p ex:inAula ex:aulaA1 ; ex:giorno "2026-10-19"^^xsd:date ;
ex:oraInizio ?i ; ex:oraFine ?f .
FILTER (STR(?i) < "12:00:00" && STR(?f) > "10:00:00")
}È la stessa regola della servlet: l'inizio esistente è prima della nuova fine (?i < 12:00) e la fine esistente è dopo il nuovo inizio (?f > 10:00). L'ASK risponde true se esiste almeno una prenotazione in conflitto: qui sì (Rossi 09:00-11:00: "09:00:00" < "12:00:00" e "11:00:00" > "10:00:00"), quindi la risposta è true: l'aula non è libera. Con 11:00-14:00 la risposta sarebbe false: Rossi finisce alle 11:00 (non è maggiore di 11:00:00) e Bianchi comincia alle 14:00 (non è minore di 14:00:00); con A1 il 20 ottobre, per qualunque orario, false. Il confronto è fatto sulle stringhe ISO (STR), perché SPARQL non garantisce un ordine per i xsd:time: le ore in formato hh:mm:ss si ordinano come testo.
3. Inferenza e collegamenti
- RDFS.
ex:inAula rdfs:domain ex:Prenotazionesignifica: il soggetto di una tripla con predicatoex:inAulaè unaex:Prenotazione. Daex:pren5 ex:inAula ex:labInfoun ragionatore deduceex:pren5 a ex:Prenotazione(anche se non è scritto). Conrdfs:range ex:Auladeduce ancheex:labInfo a ex:Aula. Sono deduzioni, non controlli: seex:pren5fosse dichiarata comeex:Aula, RDFS non segnalerebbe un errore, dedurrebbe che è anche unaex:Prenotazione. owl:sameAs. Afferma che due IRI indicano la stessa cosa, e le fa trattare come una sola: tutto ciò che si sa dell'uno vale per l'altro. Per collegare l'ateneo a DBpedia:ex:ateneo owl:sameAs <http://dbpedia.org/resource/University_of_Padua> .(con il prefissoowl:dichiarato). È l'applicazione della quarta regola dei linked data (includere collegamenti ad altri dati).
4. Confronto con SQL
SELECT a.nome
FROM aula a
WHERE NOT EXISTS (SELECT 1 FROM prenotazione p
WHERE p.aula_id = a.id AND p.giorno = '2026-10-19');Differenze: in SQL il legame fra aula e prenotazione è la condizione p.aula_id = a.id (join sul valore di una chiave esterna, dentro lo schema di una base di dati); in SPARQL è la stessa variabile ?aula in due triple, e le triple possono stare in dataset diversi, perché l'IRI è globale. SQL lavora su schemi fissi dichiarati prima; in SPARQL l'assenza di un dato è l'assenza di una tripla. Il risultato è lo stesso: Aula B2 e Lab Info.
Versione ripasso
Dati. Aule: A1 (80 posti), B2 (45), Lab Info (30). Prenotazioni: 1 = A1, 2026-10-19, 09:00-11:00, Rossi; 2 = A1, 2026-10-19, 14:00-16:00, Bianchi; 3 = B2, 2026-10-20, 10:00-12:00, Verdi; 4 = Lab Info, 2026-10-21, 09:00-13:00, Rossi.
Turtle. Le chiavi diventano IRI (ex:aulaA1, ex:pren1); la chiave esterna aula_id diventa l'arco ex:inAula verso l'IRI dell'aula. a = rdf:type, ; ripete il soggetto, 80 è xsd:integer, giorno e ore sono letterali tipizzati, nome come rdfs:label con lingua. Nessun NULL: un dato assente è una tripla assente.
@prefix ex: <http://example.org/aule/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:inAula a rdf:Property ; rdfs:domain ex:Prenotazione ; rdfs:range ex:Aula .
ex:aulaA1 a ex:Aula ; rdfs:label "Aula A1"@it ; ex:posti 80 .
ex:pren1 a ex:Prenotazione ; ex:inAula ex:aulaA1 ; ex:giorno "2026-10-19"^^xsd:date ;
ex:oraInizio "09:00:00"^^xsd:time ; ex:oraFine "11:00:00"^^xsd:time ; ex:studente "Rossi" .Conteggio: vocabolario 2 classi 2 e 6 proprietà 3 ; aule 3 3 ; prenotazioni 4 6 . Totale 55 triple.
Query (prefissi ex:, rdfs:, xsd:).
SELECT ?nome ?posti # (a)
WHERE { ?aula a ex:Aula ; rdfs:label ?nome ; ex:posti ?posti . FILTER (?posti >= 45) }
ORDER BY DESC(?posti)Risultato: Aula A1 80, Aula B2 45.
SELECT ?nome # (b)
WHERE {
?aula a ex:Aula ; rdfs:label ?nome .
FILTER NOT EXISTS { ?p ex:inAula ?aula ; ex:giorno "2026-10-19"^^xsd:date }
}Risultato: Aula B2, Lab Info (A1 ha due prenotazioni il 19).
SELECT ?studente (COUNT(?p) AS ?n) # (c)
WHERE { ?p a ex:Prenotazione ; ex:studente ?studente }
GROUP BY ?studente
HAVING (COUNT(?p) > 1)Risultato: Rossi 2.
ASK { # (d)
?p ex:inAula ex:aulaA1 ; ex:giorno "2026-10-19"^^xsd:date ; ex:oraInizio ?i ; ex:oraFine ?f .
FILTER (STR(?i) < "12:00:00" && STR(?f) > "10:00:00")
}true per 10:00-12:00 (Rossi 09:00-11:00 in conflitto); false per 11:00-14:00 e per A1 il 20. Il confronto è fra stringhe ISO (STR) perché SPARQL non garantisce un ordine sui xsd:time. Regola: inizio esistente prima della nuova fine e fine esistente dopo il nuovo inizio.
RDFS e OWL. ex:inAula rdfs:domain ex:Prenotazione + ex:pren5 ex:inAula ex:labInfo si deduce ex:pren5 a ex:Prenotazione (e con rdfs:range ex:Aula anche ex:labInfo a ex:Aula): deduzioni, non controlli. owl:sameAs: due IRI, stessa cosa; ex:ateneo owl:sameAs <http://dbpedia.org/resource/University_of_Padua> . (quarta regola dei linked data).
Confronto con SQL per (b): NOT EXISTS con la condizione p.aula_id = a.id AND p.giorno = '2026-10-19'. In SQL il legame è un join sul valore della chiave esterna in un solo schema fisso; in SPARQL è la stessa variabile ?aula in due triple, anche di dataset diversi (IRI globali). Stesso risultato.
Errori tipici
- Letterale al posto dell'IRI (
"Aula A1"invece diex:aulaA1) o viceversa. - Punto finale dimenticato,
;e,scambiati, prefisso non dichiarato. - Leggere domain e range come vincoli; credere che l'assenza di una tripla significhi "falso".
ORDER BYe confronti suixsd:timesenzaSTR; variabili con nomi diversi dove serve il join.