🌐 English · Japanese
Q1. Sales, one row per sale:
east 3
west 4
east 5
Total per region?
A1. east 8, west 4. You grouped, then you summed. Everyone can — the question is saying it without writing a hash map by hand.
Q2. Say it in Align, over a soa with k (region id) and v:
A2.
g := s.group_by(.k).sum(.v)
Group by one field, fold another. That single line is the analytics workhorse.
Q3. What comes back?
A3. Two columns, as a pair: g.0 the distinct keys, g.1 each key's total — row i of one matches row i of the other. Columns in, columns out; chapter 9 never stopped applying.
Q4. With the sales above, what are g.0.count() and g.1.sum()?
A4. 2 (east, west) and 12 (all sales — grouping only rearranged the same numbers).
Q5. Besides sum, what may follow a group_by?
A5. min(.f), max(.f), count() — smallest sale, biggest sale, and sales count per region. (count takes no field; counting needs none.)
Q6. May group_by stand alone — grouping now, aggregating later?
A6. No — same law as chapter 5: a bare group_by is an unfinished sentence (a hidden table of pieces). Say what to fold, in the same breath.
Q7. Keys that are names, and three questions at once — sum, max, count. Three passes?
A7. One:
g := xs.group_by(.name).agg(sum(.a), max(.b), count())
agg folds all three per key in a single pass: g.0 the names, g.1 the sums, then the maxes, the counts. One trip through memory, every accumulator riding along.
Mind the source. The fused multi-aggregate reads a row-shaped array<Struct> with a string key — xs here is an owned array<Row>, the kind json.decode hands back (Q12). A soa, or an integer key, is not the shape agg takes today; single-aggregate group_by(.k).sum(.v) is the one that also rides a soa (Q2).
Q8. Why does one pass matter so much?
A8. Reading a large table repeatedly can be costly. A shared pass reads each row once and updates all the aggregates for its key. Hashing and the aggregate calculations still take work.
Q9. We group by name — strings — five different times. What is silently expensive?
A9. Hashing and comparing the same strings, five times over.
Q10. The cure?
A10. Pay once:
e := xs.dict_encode(.name) // intern the names → small ids
s := e.group_by(.name).sum(.a) // these ride the ids —
c := e.group_by(.name).count() // no re-hashing
Dictionary encoding replaces repeated names with reusable IDs. The source shape is the same as for agg: xs is an owned array<Row> and .name is its string key.
Q11. Columnar databases keep appearing in our answers. Coincidence?
A11. No. Column layouts, grouped reductions, and dictionary encoding are useful for the same kinds of workloads in a database or an Align program. Here we can request those operations directly in the source.
Q12. Drill, from a JSON string of {"name":..., "a":..., "b":...} rows: distinct names, and the largest b per name — one pass.
A12.
xs: array<Row> := json.decode(data)?
g := xs.group_by(.name).agg(max(.b), count())
print(g.0.count()) // how many names
print(g.1.max()) // the largest of the per-name maxima
(The grouping computes each name's maximum and count in the same pass. The two prints show the number of distinct names and the largest of their maxima.)
Q13. Does “one pass” mean “no extra memory”?
A13. No. Grouping needs a table of accumulators and then its result columns — roughly one entry per distinct key. agg shares that table and the trip through the input; it does not make the groups cease to exist.
Q14. What if every row has a different key?
A14. Then the grouped result is nearly as large as the input. The operation is still direct and visible, but not free. Always ask two sizes: how many rows? and how many groups?
Q15. Should we dict_encode before every string grouping?
A15. No. A single string group_by already does the necessary interning for that aggregation. dict_encode earns its place when the encoded column will be reused for several groupings or comparisons. Pay once only when there really is a twice.
Q16. Orders by customer:
ada 10
bo 4
ada 3
bo 8
ada 2
What per-customer sums must come back?
A16. Customer ada → 15; customer bo → 12. Do the grouping in your head before touching syntax.
Q17. Say it over fields .customer and .amount.
A17. First say where the rows live — A7's shape, an owned array<Order> with a string key:
orders: array<Order> := json.decode(data)?
g := orders.group_by(.customer).sum(.amount)
g.0 holds the customers; g.1 holds the aligned sums.
Q18. We also need each customer's largest order and order count. Three groupings?
A18. One:
g := orders.group_by(.customer).agg(
sum(.amount),
max(.amount),
count(),
)
One key table, three accumulators per group. This is why Q16 uses customer names: agg takes a string key over an array of records. For numeric keys, use the single-aggregate forms separately.
Q19. What is the sum of g.1, the per-customer sums?
A19. 27, the same as the sum of every input amount. Grouping changes association, not the total contribution.
Q20. What is the maximum of the per-customer maxima?
A20. 10, the largest order in the whole input. A reduction may follow a grouped reduction; just keep track of which column now flows.
Q21. Five reports reuse customer names, but each report groups a different value. What is the shape of the optimization?
A21.
encoded := orders.dict_encode(.customer)
sales := encoded.group_by(.customer).sum(.amount)
counts := encoded.group_by(.customer).count()
Encode the repeated key column once, then ask several grouped questions. The reusable thing is not a half-group_by; it is the explicitly encoded data.
The Tenth Commandment
Group and fold in one breath. Ask all your questions in one pass, and pay for a string key once.