Awk Associative Arrays as a Streaming Group-By Engine
Awk associative arrays can replace sort | uniq pipelines for log aggregation, trading disk I/O for memory. Learn the single-pass pattern, key coercion rules, and cardinality limits.
09 May 2026, 15:08 UTC

The sort | uniq reflex
When you need counts from a log file—status codes, IP addresses, user agents—the common reflex is to extract a field, pipe it through sort, and count duplicates with uniq -c. That works, but sort buffers the entire input, often spilling to disk. If your log is large and the number of distinct values is small, you are doing far more I/O than the problem requires.
Awk offers a different path. Its associative arrays behave like hash maps, letting you aggregate in a single streaming pass. Memory grows with the number of unique keys, not the number of lines.
Hash maps that stream
An awk associative array is a hash map where every key is a string. You do not declare the key space, and you can index by any field—an IP address, a status code, a user agent string. The syntax is minimal:
count[$3]++
If $3 has never been seen, awk creates the entry with an empty value, which evaluates as zero in numeric context, then increments it. There is no schema to define and no separate index to maintain.
Awk reads input record by record. For each line, it applies your rules and moves on. When you accumulate counts in an array, the only state retained between records is the set of unique keys and their tallies. A million-line log with fifty distinct status codes holds fifty counters, not a million lines. This makes awk attractive for large files when cardinality—the number of distinct values in your grouping field—is low.
Order, SUBSEP, and numeric keys
POSIX specifies that the order of for (key in array) is unspecified. Two runs on the same input can produce different output orders. If a downstream consumer expects sorted results, you must pipe through sort or use a GNU awk extension such as asorti. Relying on insertion order or alphabetical order is a portability bug waiting to surface.
Multidimensional syntax like a[i,j] is shorthand for a[i SUBSEP j], where SUBSEP is a non-printing control character. This prevents accidental collisions in most real data, but it means the stored key is not the readable pair you typed.
Numeric keys are stringified through CONVFMT before hashing. The values 1.0 and 1 become the same string key, so a[1.0]=1; a[1]=2 leaves a single entry. If exact textual representation matters—version numbers like 0.1 versus .1—force string keys with explicit quotes: a["0.1"].
Worked example: counting status codes
Assume an Apache-style access.log where whitespace-separated field 9 is the HTTP status code. You need read permission on the file; no special privileges are required.
Run this in your shell:
awk '{count[$9]++} END {for (code in count) print code, count[code]}' access.log
The output order is arbitrary. To compare against the traditional pipeline, sort the awk output numerically:
awk '{count[$9]++} END {for (code in count) print code, count[code]}' access.log | sort -n
Verify the counts match:
awk '{print $9}' access.log | sort | uniq -c | sort -n
The two sorted outputs should match. If they diverge, check that field 9 is truly the status code in your log format—some servers place it elsewhere.
When memory becomes the bottleneck
The single-pass advantage reverses when cardinality is high. Grouping by a full timestamp, a unique request ID, or an entire URL means every record may introduce a new key. The array grows until it holds nearly as many entries as the file has lines, and you lose the memory benefit while still avoiding sort. On a constrained host, that can exhaust RAM.
A practical check before scaling up: sample the file and count distinct values in your grouping field. If distinct values are a small fraction of total lines, awk aggregation is usually safe. If they approach the line count, prefer an external tool designed for disk-backed sorting.
Decision guide
Use awk associative arrays when you need fast, single-pass aggregation over large inputs and the grouping field has naturally low cardinality—status codes, small sets of IPs, coarse error categories. Accept that output order is unspecified and pipe to sort when order matters. When cardinality is high or memory is tight, fall back to sort | uniq -c and let the operating system manage spill files.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.