Salta al contenuto
Note per Studenti Esercizio - Triple RDF in Turtle e query SPARQL sulle aule (applicazione web)

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
  1. Scrivere in Turtle il vocabolario (classi Aula e Prenotazione, proprietà con domain e range) e i dati, con prefisso ex: = http://example.org/aule/. Dire quante triple risultano e disegnare il grafo di una prenotazione.
  2. 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?
  3. Quali triple si deducono con RDFS da ex:inAula rdfs:domain ex:Prenotazione se si aggiunge ex:pren5 ex:inAula ex:labInfo? Che cosa dice owl:sameAs e come si collegherebbe l'ateneo a DBpedia?
  4. Confrontare la query (b) con l'equivalente SQL.

Soluzione

1. Il dataset in Turtle

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 = 1 diventa il soggetto ex:aulaA1, e prenotazione.id = 1 diventa ex:pren1. La chiave esterna aula_id non è più un numero da confrontare con un join, ma un arco ex:inAula il cui oggetto è l'IRI dell'aula (ex:pren1 ex:inAula ex:aulaA1).
  • a = rdf:type. ; ripete il soggetto. I numeri senza virgolette (80) sono xsd:integer. Giorno e ore sono letterali tipizzati (xsd:date, xsd:time); il nome dell'aula è un'etichetta rdfs:label con lingua.
  • Conteggio. Il vocabolario ha 2 classi ×\times 2 triple (tipo ed etichetta) =4=4 e 6 proprietà ×\times 3 triple (tipo, domain, range) =18=18; le 3 aule hanno 3 triple ciascuna =9=9 (tipo, etichetta, posti); le 4 prenotazioni hanno 6 triple ciascuna =24=24 (tipo, aula, giorno, inizio, fine, studente). Totale 4+18+9+24=554+18+9+24=55 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:

sparql
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.

sparql
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.

sparql
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.

sparql
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?

sparql
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:Prenotazione significa: il soggetto di una tripla con predicato ex:inAula è una ex:Prenotazione. Da ex:pren5 ex:inAula ex:labInfo un ragionatore deduce ex:pren5 a ex:Prenotazione (anche se non è scritto). Con rdfs:range ex:Aula deduce anche ex:labInfo a ex:Aula. Sono deduzioni, non controlli: se ex:pren5 fosse dichiarata come ex:Aula, RDFS non segnalerebbe un errore, dedurrebbe che è anche una ex: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 prefisso owl: dichiarato). È l'applicazione della quarta regola dei linked data (includere collegamenti ad altri dati).

4. Confronto con SQL

L'equivalente della query (b) in SQL (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 →):

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.

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#> .

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 ×\times 2 =4=4 e 6 proprietà ×\times 3 =18=18; aule 3 ×\times 3 =9=9; prenotazioni 4 ×\times 6 =24=24. Totale 55 triple.

Query (prefissi ex:, rdfs:, xsd:).

sparql
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.

sparql
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).

sparql
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.

sparql
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 →\to 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 di ex: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 BY e confronti sui xsd:time senza STR; variabili con nomi diversi dove serve il join.

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 →.

Teoria collegata