Aggregation
count, sum and list over groups, filtering groups with having, and an aggregate in a rule's head.
An aggregate in the projection reduces many rows to one per group. The plain variables in the projection are the group, and the aggregate is computed over each group’s rows. This counts what each package imports:
imports(?p, ?q) => ?p, count(?q)| ?p | count(?q) |
|---|---|
| api | 4 |
| app | 3 |
| auth | 3 |
| cache | 2 |
| config | 1 |
| db | 2 |
| legacy | 1 |
| log | 1 |
| metrics | 1 |
deps datasetutil has no row because it imports nothing, so there’s no group for it, which is worth remembering when a count you expected to be zero is just missing. Several aggregates can share a group, which is a fairly common shape of question. Here’s each team’s package count and total lines:
owner(?p, ?t), loc(?p, ?n) => ?t, count(?p), sum(?n)| ?t | count(?p) | sum(?n) |
|---|---|---|
| data | 2 | 1900 |
| platform | 4 | 700 |
| security | 1 | 800 |
| web | 3 | 2400 |
deps datasetThe aggregates are count, sum, min, max and list.
Counting rows or values ¶
count(?t) counts the rows in a group, and count(distinct ?p) counts the different values. They differ when a value repeats. The security team has two tests, both of auth:
test(?t, ?p), owner(?p, ?team) => ?team, count(?t), count(distinct ?p)| ?team | count(?t) | count(distinct ?p) |
|---|---|---|
| data | 1 | 1 |
| platform | 1 | 1 |
| security | 2 | 1 |
| web | 1 | 1 |
deps datasetlist gathers a group’s values into a list, in the answer’s usual order, which a later query can read again with the list.* functions (reference):
owner(?p, ?t) => ?t, list(?p)| ?t | list(?p) |
|---|---|
| data | ["cache","db"] |
| platform | ["config","log","metrics","util"] |
| security | ["auth"] |
| web | ["api","app","legacy"] |
deps datasetFiltering groups ¶
A comparison in the goal filters rows before they’re grouped. To filter the groups themselves, by what an aggregate came to, add having after the projection. These are the packages at least three others import:
imports(?p, ?q) => ?q, count(?p) having count(?p) >= 3| ?q | count(?p) |
|---|---|
| config | 4 |
| log | 5 |
| util | 3 |
deps datasetAn aggregate in a rule ¶
A rule’s head can aggregate, which gives the result a name that other rules and goals can use like any relation. fan_in is how many packages import each one, and the goal then reads it with an ordinary comparison:
fan_in(?q, count(?p)) :- imports(?p, ?q);
fan_in(?q, ?n), ?n >= 3 => ?q, ?n| ?q | ?n |
|---|---|
| config | 4 |
| log | 5 |
| util | 3 |
deps datasetA rule that aggregates has to be its relation’s only rule. Two rules each producing a count for the same package would leave it with two counts, and it’s not clear which one you’d want. jaala derives everything the rule’s body reads before it computes the aggregate, the same way it handles not, so the count is never taken over a relation that’s still growing.
A group with no rows ¶
With no plain variables in the projection, the whole answer is one group, and an aggregate over no rows still answers. legacy imports nothing the security team owns, so the count is zero rather than missing:
That’s the end of the language tutorials. Everything here also works from Go, which the next tutorial covers.