Paste this at the top of every Turtle file and SPARQL query. The first eight are the curriculum default; PROV-O and OWL-Time get added when reification or temporal modeling shows up.
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . @prefix owl: <http://www.w3.org/2002/07/owl#> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . @prefix dcterms: <http://purl.org/dc/terms/> . @prefix foaf: <http://xmlns.com/foaf/0.1/> . @prefix schema: <https://schema.org/> . @prefix skos: <http://www.w3.org/2004/02/skos/core#> . # Add when needed: @prefix prov: <http://www.w3.org/ns/prov#> . @prefix time: <http://www.w3.org/2006/time#> .
schema: resolves to https://schema.org/ with the trailing slash. Older tutorials use http://; both work but the spec is now https.
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> PREFIX schema: <https://schema.org/> PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
Every Turtle file is a set of triples (subject, predicate, object) ending in a period. The shorthand is what makes it readable at scale.
:Naruto schema:memberOf :Konoha .
:Naruto a schema:Person ;
schema:memberOf :Konoha ;
schema:name "Naruto Uzumaki" .
:Naruto naruto:hasJutsu :ShadowClone ,
:Rasengan ,
:SageMode .
:Naruto a schema:Person .
# is the same as:
:Naruto rdf:type schema:Person .
a keyword is shorthand for rdf:type. Use it."Naruto Uzumaki" # plain string "Naruto Uzumaki"@en # language-tagged "うずまきナルト"@ja # Japanese "1999-09-21"^^xsd:date # typed 42 # integer (sugar for "42"^^xsd:integer) 3.14 # decimal true # boolean
:Itachi schema:description """ Member of the Uchiha clan who joined Akatsuki after the Uchiha massacre. """ .
:Naruto schema:address [ schema:streetAddress "Konoha Village" ; schema:postalCode "00001" ] .
:ChuninExams naruto:teams ( :Team7 :Team8 :Team10 ) .
Blank nodes are local. Two blank nodes from different files cannot be assumed identical even if they have identical properties. If you'll need to reference it from outside, use a real URI.
There are dozens. These are the ones that come up in real ontology work.
| Datatype | What for | Example |
|---|---|---|
| xsd:string | text, when language doesn't matter | "Konoha"^^xsd:string |
| xsd:integer | whole numbers | "42"^^xsd:integer or just 42 |
| xsd:decimal | exact decimals (money, ratios) | "3.14"^^xsd:decimal |
| xsd:double | floating point (scientific) | "6.022e23"^^xsd:double |
| xsd:boolean | true/false | true |
| xsd:date | calendar date (no time) | "2026-05-26"^^xsd:date |
| xsd:dateTime | date plus time, ISO 8601 | "2026-05-26T14:30:00Z"^^xsd:dateTime |
| xsd:gYear | year only | "1999"^^xsd:gYear |
| xsd:anyURI | URI as a literal (not a node) | "https://barbhs.com"^^xsd:anyURI |
SPARQL is graph-pattern matching. You describe a shape; the engine finds every binding of variables that fits the shape. The mental shift from SQL is real — but the syntax is small.
| SELECT | Return a table of variable bindings. The default. |
| ASK | Return true/false — does at least one match exist? |
| CONSTRUCT | Return a new RDF graph built from the matches. Used to materialize inferences. |
| DESCRIBE | Return everything the endpoint knows about a resource. |
PREFIX schema: <https://schema.org/> SELECT ?person ?name WHERE { ?person a schema:Person ; schema:name ?name . } ORDER BY ?name LIMIT 25
Variables start with ?. Each triple pattern ends in a period (or chains with ; / , like Turtle).
?s rdf:type ?type . # what types is ?s? ?s ?p ?o . # everything about ?s ?ninja schema:memberOf :Konoha . # everyone from Konoha
?character schema:birthDate ?date . FILTER (?date < "1990-01-01"^^xsd:date) ?character schema:name ?name . FILTER (REGEX(?name, "Uchiha", "i")) # case-insensitive FILTER EXISTS { ?character naruto:hasJutsu :Sharingan } FILTER NOT EXISTS { ?character naruto:rivalOf :Naruto }
?person schema:name ?name . OPTIONAL { ?person schema:email ?email } OPTIONAL { ?person schema:birthDate ?dob }
If the optional pattern matches, the variable is bound. If not, the row still returns with that variable unbound.
{ ?person schema:name ?name }
UNION
{ ?person foaf:name ?name }
SELECT ?village (COUNT(?ninja) AS ?count) WHERE { ?ninja schema:memberOf ?village . } GROUP BY ?village HAVING (?count > 5) ORDER BY DESC(?count)
Other aggregates: SUM, AVG, MIN, MAX, GROUP_CONCAT, SAMPLE.
?p foaf:knows+ :Naruto . # 1 or more hops ?p foaf:knows* :Naruto . # 0 or more (includes self) ?p foaf:knows? :Naruto . # 0 or 1 (optional) ?p (foaf:knows|schema:colleague) :Naruto . # alternation ?p schema:parent/schema:name ?gpName . # sequence: grandparent's name ?p ^schema:parent ?child . # inverse: ?p is ?child's parent
CONSTRUCT { ?person naruto:hasCompetency ?skill . } WHERE { ?person naruto:heldRole ?role . ?role schema:duration ?d . ?role naruto:relatedSkill ?skill . FILTER (?d > "P2Y"^^xsd:duration) }
Returns RDF, not a table. The workhorse of inference materialization in Module 3.
A query that returns zero results doesn't mean your data is wrong. The open-world model treats absence of evidence as just absence, not falsehood. Check the pattern first — variable names that don't match, wrong predicate, missing prefix declaration.
Wikidata uses Q-numbers for entities and P-numbers for properties. The query service at query.wikidata.org has a forgiving UI — find Q/P numbers via the search box, then modify example queries before writing from scratch.
PREFIX wd: <http://www.wikidata.org/entity/> PREFIX wdt: <http://www.wikidata.org/prop/direct/> # Books by Adrian Tchaikovsky (Q14010003) SELECT ?book ?title WHERE { ?book wdt:P50 wd:Q14010003 ; # P50 = author wdt:P31 wd:Q571 ; # P31 = instance of, Q571 = book rdfs:label ?title . FILTER(LANG(?title) = "en") }
SELECT ?person ?personLabel WHERE { ?person wdt:P69 wd:Q49108 . # educated at MIT SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . } }
Any variable ending in Label gets auto-resolved to a readable name.
| wdt:P31 | instance of |
| wdt:P279 | subclass of |
| wdt:P50 | author |
| wdt:P69 | educated at |
| wdt:P106 | occupation |
| wdt:P569 | date of birth |
| wd:Q5 | human |
| wd:Q571 | book |
Wikidata times out on complex queries. Start narrow (constrain to one entity type first), then expand. Use LIMIT 100 while iterating.
Reuse before defining. Every custom term is future maintenance burden and lost interoperability. The order below is the order to check.
| Prefix | Reach for it when… | Examples |
|---|---|---|
| rdfs: | You need labels, comments, or class/subclass hierarchy | rdfs:label, rdfs:comment, rdfs:subClassOf |
| schema: | Modeling people, organizations, events, places, books, articles — almost everything mainstream | schema:Person, schema:Organization, schema:Book, schema:TVEpisode |
| foaf: | People and social relations, when schema.org doesn't fit | foaf:Person, foaf:knows, foaf:homepage |
| skos: | Concept schemes, taxonomies, controlled vocabularies (ESCO, MeSH, AAT) | skos:Concept, skos:broader, skos:exactMatch |
| dcterms: | Bibliographic metadata about resources — source, date, creator, license | dcterms:source, dcterms:created, dcterms:creator |
| prov: | Who said what, when, derived from where | prov:wasAttributedTo, prov:wasGeneratedBy, prov:Activity |
| time: | Time intervals, durations, before/after relations | time:Interval, time:hasBeginning, time:before |
| owl: | Class equivalence, property characteristics, restrictions — Module 2+ | owl:Class, owl:sameAs, owl:equivalentClass |
REUSE.mdThe errors you'll hit in the first month. Each one looks cryptic the first time.
A Turtle statement that ends without ., or a continuation that ends with a dangling ; or ,, fails to parse. Always check the line above the parse error too — Turtle errors often surface one line late.
schema:The canonical prefix is https://schema.org/, not http://schema.org/. Both resolve over the web, but mixing them within a single graph creates two distinct URIs that look identical to you and different to the engine.
A blank node in file A is not the same as a blank node in file B even if every property matches. If a thing needs to be referenced from outside its file, give it a real URI.
FILTER only constrains already-bound variables. It cannot introduce a variable. If your query says FILTER(?x = "y") but nothing else binds ?x, the filter operates on unbound and produces nothing.
owl:sameAs is a hammerAsserting :A owl:sameAs :B tells the reasoner to merge every property of both URIs. Use skos:exactMatch when you mean "same referent, keep attributes separate."
"Person X has no listed children" does not mean "Person X has zero children." SPARQL queries that look for absence return empty, and OWL reasoners refuse to infer falsehood from missing data. Layer SHACL when you need closed-world for application logic.
URIs are case-sensitive. :Naruto and :naruto are different nodes. Pick a convention (capitalize classes and instances, lowercase properties) and enforce it.
When a SPARQL query returns nothing, the first instinct is to blame the data. Usually it's the pattern — a typo in a prefix, a missing predicate, a triple that lives in a named graph the query didn't include. Verify with a broader pattern first.
| Tool | What it's for |
|---|---|
| Local Apache Jena Fuseki | Your default development triplestore. Runs on localhost:3030 after install. |
| Wikidata Query Service | The largest public SPARQL endpoint. Helper UI is forgiving; modify example queries. |
| DBpedia SPARQL | Wikipedia-derived structured data. Older than Wikidata, sometimes faster. |
| RDF Playground | In-browser sandbox. Write Turtle on the left, visualize as a graph, run SPARQL or SHACL. Best teaching tool for newcomers. |
| WebVOWL | Drag-and-drop ontology visualization. Generate publication-ready screenshots without installing anything. |
| Protégé | Desktop ontology editor. The de facto standard for OWL design work. Save in Turtle, not RDF/XML. |