Chapter 18: Asking Questions
Friday afternoon. The board wanted numbers: how many patients of each species, which vets had no one on their list, which dog owners could be reached by email. The data was all there, typed, checked and linked. Dr. Portbridge just needed a way to ask.
The problem
Facts and mappings fill the graph. Rules add to it. But nothing so far reads it back. In Chapter 12 a downstream system found the senior cats with a SPARQL query, written by hand, in another language. Dolfin has its own way to ask: the query.
A first query
query all_cats:
?cat a Cat
name ?name
age ?age
This reads like the shape it looks for: a cat, with a name and an age.
Every cat that has both gives one row, with columns cat, name and age.
A query is translated to SPARQL. This one becomes:
SELECT ?cat ?name ?age
WHERE {
?cat a Cat .
?cat name ?name .
?cat age ?age .
}
The indented lines all talk about the subject above them, so ?cat is written
once instead of on every line. Patterns are combined with implicit AND, as in
the match: block of a rule.
Choosing the columns
The board wants the five oldest cats past ten, oldest first. A constraint
filters, and a return block shapes the result:
query senior_cats:
?cat a Cat
name ?name
age [?age >= 10]
return
?name
?age
order desc
limit 5
age [?age >= 10]binds the age to?ageand keeps only ages of 10 and more.returnlists the columns.?catis not in it, so it is not returned.order descunder a column sorts on it;limit 5keeps five rows.
Without a return block, every variable is returned. A variable that starts
with ?_ is silent: it is used for matching but never returned.
Following links
Owners are not animals: they hang off the owner property. A [ … ] block
opens a nested subject, without naming it:
query owners_of_dogs:
a Dog
name ?dogName
owner [
lastName ?ownerName
optional email ?email
]
The dog has no variable either: a Dog alone is enough when nothing else
refers to it. email is optional in the schema, so the line is marked
optional: an owner without an email still gives a row, with an empty
email column. Without the keyword, those owners would vanish from the result.
Links can be followed backwards. treatedBy goes from an animal to a vet; to
start from the vet, read it with is … of:
?vet is treatedBy of ?animal
Something, or nothing
Which vets have no patients? This is a question about something that is
not there. A none: block keeps a row only if its content never matches:
query vets_without_patients:
?vet a Veterinarian
name ?name
none:
?vet is treatedBy of ?_animal
some: is the opposite: the row stays if the content matches at least once.
Inside both, ?vet is the same vet as outside; ?_animal is local.
And when either of two conditions will do, either … or says so:
query heavy_or_old:
?pet a Animal
name ?name
either weight [ > 30.0 ]
or age [ > 12 ]
Counting
How many patients of each species? A group by block groups the rows and
computes one value per group:
query animals_per_species:
?animal a Animal
species ?species
group by ?species
count ?animal as ?total
?total >= 2
return
?species
?total
order desc
count ?animal as ?total counts the animals in each species. The line
?total >= 2 drops species with a single patient. sum, average, min
and max work the same way as count. count distinct ?x counts each value of
?x once: count distinct ?owner as ?owners counts owners, not the animals
that lead to them.
To count per animal while keeping one row per animal, put the count in the body, with what it counts indented below it:
query vaccine_count:
?animal a Animal
name ?name
count ?shot as ?shots
?animal vaccinations ?shot
?animal appears above the count, so each animal gets its own ?shots.
Building on other queries
A query can use another one. Write the question once, reuse it:
query cats_with_vet:
?cat a Cat
treatedBy ?vet
return
?cat
?vet
query cat_vet_names:
cats_with_vet as [
cat ?c
vet ?v
]
?c name ?catName
?v name ?vetName
cats_with_vet as [ … ] runs the first query and binds its cat and vet
columns to ?c and ?v. A query with a single column binds in one line:
some_query as ?x.
Summary
| Write | To |
|---|---|
query name: | Declare a query |
?x a Type / a Type | Look for instances, named or not |
prop ?v | Read a property |
prop [?v > 3] | Read and filter it |
optional prop ?v | Read it if it is there |
prop [ … ] | Follow a link |
is prop of ?x | Follow a link backwards |
some: / none: | Require that something exists, or does not |
either … or | Accept any of several conditions |
group by ?x + count ?y as ?n | Count, sum, average per group |
return | Choose, rename, sort and limit the columns |
other_query as [ … ] | Reuse a query |
The syntax reference has every form, and the SPARQL each one becomes.
The board meeting lasted twenty minutes. Dr. Portbridge answered every question by running a query she had written that morning. When the treasurer asked for one more number, she wrote a new one on the spot, four lines long.