SPARQL: aggregation¶
So far every query has returned one row per match. Aggregation collapses many rows into one: how many, how much, the largest, the whole list as a single value.
This is the chapter where SPARQL surprises people. The grouping rules are implicit by default, and when they misfire the query still runs and still returns a table, just not the one you meant. This chapter spends most of its length on that.
This chapter assumes SPARQL: the basics and graph patterns.
What aggregation does¶
An aggregation is a function applied to a group of values. The groups are formed by taking every unique combination of values for a chosen set of variables.
An example makes this concrete. In the Pokémon dataset, each Pokémon has a name in several languages, so a plain query returns several rows per Pokémon:
?pokemon |
?name |
|---|---|
id:abomasnow |
"ABOMASNOW"@it-it |
id:abomasnow |
"BLIZZAROI"@fr-fr |
id:abomasnow |
"ユキノオー"@ja-ja |
id:abra |
"ABRA"@en-us |
id:abra |
"ケーシィ"@ja-ja |
Group by ?pokemon and the set of names for each Pokémon becomes one group. You
can then count them, list them, or take the longest.
Counting, explicitly¶
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
select ?pokemon (count(?name) as ?numberOfNames)
where {
?pokemon vocab:name ?name.
}
group by ?pokemon
limit 25
Two things changed. The projection now contains an expression, which needs the
as ?variable form to give the column a name. And group by states the grouping
criterion.
Besides count, the standard functions are sum, avg, min, max,
sample (any one value from the group), and group_concat.
Grouping that you did not ask for¶
Here is the part to slow down for. If a query contains an aggregation function
and no group by, SPARQL groups anyway. It groups by every variable that is
(1) visible in the projection and (2) not an argument to an aggregation function.
Leave out group by ?pokemon from the query above and you get the same result,
because ?pokemon is visible and is not inside count(). That is convenient,
and it is also the trap: add one more variable to the projection and the grouping
silently changes underneath you.
select ?pokemon ?colour (count(?name) as ?numberOfNames)
where {
?pokemon
vocab:name ?name;
vocab:colour ?colour.
}
This no longer counts names per Pokémon. It counts names per combination of Pokémon and colour. With one colour per Pokémon the numbers happen to match; with two, they quietly halve.
The rule to remember: the projection determines the grouping. Adding a column is never cosmetic in an aggregating query.
Hiding a grouping variable¶
Grouping variables have to appear in the projection. So what do you do when you need to group by something you do not want as a column?
You cannot simply drop it. You need a subselect: an inner query that does the grouping, wrapped by an outer query that projects only what you want.
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
select ?numberOfNames
where {
{
select ?pokemon (count(?name) as ?numberOfNames)
where {
?pokemon vocab:name ?name.
}
group by ?pokemon
}
}
The inner query groups by ?pokemon; the outer query returns only the counts.
Subselects are the standard answer whenever grouping and projection want
different things.
Aggregating an aggregate¶
"Which Pokémon has the most names?" needs two aggregations in sequence: first count per Pokémon, then take the maximum of those counts. Since an aggregation function cannot take another aggregation as its argument, this is a subselect again — the inner query counts, the outer query takes the maximum.
Returning the evidence¶
max(?weight) gives you a number, not the thing that weighs it. This is a
recurring frustration: you want the heaviest Pokémon, and SPARQL hands you the
weight.
The pattern is the same subselect shape. Compute the maximum in an inner query, then match in the outer query for the resource whose value equals it. The result carries both the number and the evidence for it.
Building one string from many rows¶
group_concat turns a group of values into a single string, with a separator you
choose:
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
select ?pokemon (group_concat(?name; separator=', ') as ?names)
where {
?pokemon vocab:name ?name.
}
group by ?pokemon
limit 25
This is how you get one readable row per resource instead of five near-identical ones — useful for exports and for anything a human will read.
Filtering on the result of an aggregation¶
filter runs before grouping, so it cannot test a count. having is the version
that runs after:
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
select ?pokemon (count(?name) as ?numberOfNames)
where {
?pokemon vocab:name ?name.
}
group by ?pokemon
having (count(?name) > 5)
If you find yourself wondering why a filter on an aggregated column is ignored, this is why.
Where this appears in Triply products¶
- TriplyDB's Insights page is aggregation made visual: the class frequency view counts how often each class and property occurs in a graph. See Data views.
- Data stories commonly use aggregating queries to drive charts. See Data stories.
Where to next¶
- Construct, ask, describe and federation — the other three query forms
- Querying real-world data — hierarchies, geodata and statistics
- Graph patterns — back one chapter
- SPARQL: the basics — back to the first chapter
- Glossary — every term on this page, in one place