Skip to content

Arrays and Aggregation

Individual values are chapter 2's business. This chapter is about arrays as a whole: transforming every element at once, sorting, deduplicating, totalling, and grouping. The stage is the inventory — eight machines whose RAM, disks, and roles are exactly the kind of thing these questions are asked about.

map: run a filter on every element

map(f) applies f to each element of an array and collects the results into a new array:

$ jq 'map(.ram_gb)' inventory.json
[
  64,
  32,
  32,
  16,
  128,
  64,
  8,
  16
]

map(.ram_gb) is shorthand for [.[] | .ram_gb] — the explode-and-rebuild of chapters 2 and 4, packaged. Once the numbers are an array, the aggregation builtins finish the job. add folds an array of numbers (or strings) into one value:

$ jq 'map(.ram_gb) | add' inventory.json
360

$ jq 'map(.ram_gb) | add / length' inventory.json
45

360 GB of RAM across the fleet, 45 on average — add / length is the average idiom; there is no separate avg builtin. min and max work the same way on a mapped array (map(.ram_gb) | max is 128), and min_by/max_by return the whole element that holds the extreme, which is usually the actual question:

$ jq 'min_by(.disk_free_gb) | .name' inventory.json
"garnet-07"

Sorting and deduplicating

sort orders an array; sort_by(f) orders it by what f produces for each element:

$ jq '[.[] | select(.role == "server")] | sort_by(.disk_free_gb) | map(.name)' inventory.json
[
  "garnet-07",
  "birch-02",
  "atlas-01",
  "elm-05"
]

Servers by free disk, emptiest first. reverse turns any order around:

$ jq '[.models[].name] | reverse' models.json
[
  "harbor-4:70b",
  "kestrel-1:1.5b",
  "kestrel-1:8b",
  "ember-2:32b",
  "ember-2:7b"
]

unique sorts and removes duplicates — the fleet's roles and operating systems in one line each:

$ jq 'map(.role) | unique' inventory.json
[
  "laptop",
  "server",
  "workstation"
]

$ jq 'map(.os) | unique' inventory.json
[
  "linux",
  "macos",
  "windows"
]

unique_by(f) deduplicates by a computed key and keeps the first element of each kind — here, one representative machine per operating system:

$ jq 'unique_by(.os) | map(.name)' inventory.json
[
  "atlas-01",
  "dune-04",
  "copper-03"
]

Both unique forms sort as a side effect; if the original order matters, that's a behaviour to know about, not discover later.

group_by: buckets with counts

Grouping is a three-step rhythm: sort by the key, group into runs of equal keys, then summarise each run. (Current jq actually sorts inside group_by itself — feed it shuffled input and the groups still come out whole — but the explicit sort_by costs nothing, keeps the intent visible, and is what you'll see in every example elsewhere, so this guide keeps it.)

$ jq 'sort_by(.location) | group_by(.location) | map({location: .[0].location, machines: length})' inventory.json
[
  {
    "location": "lab",
    "machines": 2
  },
  {
    "location": "office",
    "machines": 4
  },
  {
    "location": "remote",
    "machines": 2
  }
]

Inside each group — itself an array — .[0].location is the key every member shares, and length is the count. The {role: …, count: length} version of this pattern reappears in chapter 8's filter file. This is also the honest place to say that group_by is as far as this guide goes. jq has more — sort_by with several keys, to_entries acrobatics for pivot tables — but past grouping, a real programming language starts earning its keep. Chapter 11 returns to where that line sits.

flatten: nested arrays, levelled — all the way

flatten takes nested arrays and brings every element to the top level:

$ jq '.models | map(.details.families) | flatten | unique' models.json
[
  "ember",
  "harbor",
  "kestrel"
]

Each model's families is an array; map makes an array of arrays; flatten levels it; unique finishes. The honest note the name hides: bare flatten recurses all the way down, through every level of nesting —

$ jq -n '[[1, [2, 3]], 4] | flatten'
[
  1,
  2,
  3,
  4
]

— and it leaves non-array elements (including objects) untouched, but an array buried inside an object's value is not reached either; flatten works on the array structure itself. When you want exactly one level, that's flatten(1), or the explicit [.[][]] for the common two-level case. Knowing which you asked for matters the day the data nests deeper than you thought.