jq — Aggregation

Slurp Mode (-s)

Collect line-delimited JSON into an array
printf '{"a":1}\n{"a":2}\n{"a":3}\n' | jq -s '.'
# Output:
# [
#   {"a": 1},
#   {"a": 2},
#   {"a": 3}
# ]

Without -s, jq processes each line independently. With -s, it reads all input into a single array first. This is mandatory for any cross-object operation (sorting, grouping, reducing).

Slurp + sum
printf '{"bytes":1024}\n{"bytes":2048}\n{"bytes":512}\n' | jq -s 'map(.bytes) | add'
# Output: 3584
Slurp + count
printf '{"state":"UP"}\n{"state":"DOWN"}\n{"state":"UP"}\n' | jq -s 'length'
# Output: 3

sort_by

Sort objects by a field
echo '[{"name":"wlan0","mtu":1500},{"name":"lo","mtu":65536},{"name":"eth0","mtu":1500}]' | \
    jq 'sort_by(.name)'
# Output:
# [
#   {"name": "eth0", "mtu": 1500},
#   {"name": "lo", "mtu": 65536},
#   {"name": "wlan0", "mtu": 1500}
# ]
Sort descending — reverse after sort
echo '[{"name":"eth0","mtu":1500},{"name":"lo","mtu":65536},{"name":"docker0","mtu":1500}]' | \
    jq 'sort_by(.mtu) | reverse'
# Output:
# [
#   {"name": "lo", "mtu": 65536},
#   {"name": "eth0", "mtu": 1500},
#   {"name": "docker0", "mtu": 1500}
# ]
Sort by multiple keys
echo '[{"state":"UP","name":"wlan0"},{"state":"DOWN","name":"eth0"},{"state":"UP","name":"eth0"}]' | \
    jq 'sort_by(.state, .name)'
# Output:
# [
#   {"state": "DOWN", "name": "eth0"},
#   {"state": "UP", "name": "eth0"},
#   {"state": "UP", "name": "wlan0"}
# ]

group_by

Group objects by a field value
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"},{"name":"docker0","state":"DOWN"}]' | \
    jq 'group_by(.state)'
# Output:
# [
#   [{"name": "docker0", "state": "DOWN"}],
#   [{"name": "lo", "state": "UNKNOWN"}],
#   [{"name": "eth0", "state": "UP"}, {"name": "wlan0", "state": "UP"}]
# ]

group_by(.field) sorts by the field, then partitions into sub-arrays of equal values. This is jq’s GROUP BY.

Group + count — frequency table
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"},{"name":"docker0","state":"DOWN"}]' | \
    jq 'group_by(.state) | map({state: .[0].state, count: length})'
# Output:
# [
#   {"state": "DOWN", "count": 1},
#   {"state": "UNKNOWN", "count": 1},
#   {"state": "UP", "count": 2}
# ]
Group + aggregate — names per state
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"},{"name":"docker0","state":"DOWN"}]' | \
    jq 'group_by(.state) | map({state: .[0].state, interfaces: [.[].name]})'
# Output:
# [
#   {"state": "DOWN", "interfaces": ["docker0"]},
#   {"state": "UNKNOWN", "interfaces": ["lo"]},
#   {"state": "UP", "interfaces": ["eth0", "wlan0"]}
# ]

unique and unique_by

Remove duplicate values from an array
echo '["UP","DOWN","UP","UNKNOWN","UP","DOWN"]' | jq 'unique'
# Output: ["DOWN", "UNKNOWN", "UP"]
Unique by a field — deduplicate objects
echo '[{"state":"UP","name":"eth0"},{"state":"UP","name":"wlan0"},{"state":"DOWN","name":"docker0"}]' | \
    jq 'unique_by(.state)'
# Output:
# [
#   {"state": "DOWN", "name": "docker0"},
#   {"state": "UP", "name": "eth0"}
# ]

unique_by keeps the first object per unique value. If you need all objects, use group_by instead.

min_by / max_by

Find the object with the largest value
echo '[{"name":"eth0","mtu":1500},{"name":"lo","mtu":65536},{"name":"docker0","mtu":1500}]' | \
    jq 'max_by(.mtu)'
# Output: {"name": "lo", "mtu": 65536}
Find the object with the smallest value
echo '[{"name":"eth0","mtu":1500},{"name":"lo","mtu":65536},{"name":"docker0","mtu":1500}]' | \
    jq 'min_by(.mtu)'
# Output: {"name": "eth0", "mtu": 1500}
Top N by value
echo '[{"name":"a","size":100},{"name":"b","size":500},{"name":"c","size":250},{"name":"d","size":50}]' | \
    jq 'sort_by(.size) | reverse | .[:2]'
# Output:
# [
#   {"name": "b", "size": 500},
#   {"name": "c", "size": 250}
# ]

add

Sum an array of numbers
echo '[1500, 65536, 1500, 1500]' | jq 'add'
# Output: 70036
Sum a field across objects
echo '[{"name":"eth0","errors":5},{"name":"wlan0","errors":12},{"name":"lo","errors":0}]' | \
    jq 'map(.errors) | add'
# Output: 17
Merge an array of objects
echo '[{"dns":"53"},{"http":"80"},{"ssh":"22"}]' | jq 'add'
# Output:
# {
#   "dns": "53",
#   "http": "80",
#   "ssh": "22"
# }

add on an array of objects merges them. Combined with map({(key): value}), this builds objects dynamically.

Concatenate arrays
echo '[[1,2],[3,4],[5,6]]' | jq 'add'
# Output: [1, 2, 3, 4, 5, 6]

length

Count elements
echo '[{"a":1},{"a":2},{"a":3}]' | jq 'length'
# Output: 3
Count after filtering
echo '[{"name":"eth0","state":"UP"},{"name":"lo","state":"UNKNOWN"},{"name":"wlan0","state":"UP"}]' | \
    jq '[.[] | select(.state == "UP")] | length'
# Output: 2
Count keys in an object
echo '{"name":"eth0","state":"UP","mtu":1500,"mac":"aa:bb:cc:dd:ee:ff"}' | jq 'length'
# Output: 4

limit and first/last

Take first N results
echo '[1,2,3,4,5,6,7,8,9,10]' | jq '.[:3]'
# Output: [1, 2, 3]
first and last
echo '[{"name":"a"},{"name":"b"},{"name":"c"}]' | jq 'first'
# Output: {"name": "a"}

echo '[{"name":"a"},{"name":"b"},{"name":"c"}]' | jq 'last'
# Output: {"name": "c"}
Limit with early exit — nth
echo '[10,20,30,40,50]' | jq 'nth(2)'
# Output: 30

Practical: Aggregation Pipeline

Complete aggregation example — process list summary
ps aux --no-headers | awk '{print "{\"user\":\""$1"\",\"cpu\":"$3",\"mem\":"$4",\"cmd\":\""$11"\"}"}' | \
    jq -s 'group_by(.user) | map({
        user: .[0].user,
        processes: length,
        total_cpu: (map(.cpu) | add),
        total_mem: (map(.mem) | add)
    }) | sort_by(.total_cpu) | reverse | .[:5]'
# Output: top 5 users by CPU, with process count and memory totals

This is the full pattern: shell command produces JSON lines, -s collects them, then group/aggregate/sort/slice.