Relix

Problem solving

As columns, as a list, as a tree

Grain: changes on purpose — per group (nest/pivot), per element (unnest), per root (tree) · Class: Reshaping · Signals: as columns, as a list, gather, flatten, as a tree, wide, nested · Operators: PIVOT/UNPIVOT, COLLECT, μ, TREE

The problem

"Turn the long quarterly sales table into one column per quarter. Gather each customer's order ids into a list. Flatten a stored list back into rows. And fold an org chart's parent pointers into a nested tree."

How to recognise it

The question is about shape, not selection or summary — as columns, as a list, flatten, as a tree, wide instead of long. The tell is that the grain changes on purpose: reshaping deliberately turns one row-per-thing into a different one-row-per-something. Which operator depends on the target shape:

Recipe 1: rows to columns (PIVOT)

PIVOT value BY key turns each distinct value of the key column into an output column. A group with no row for a key gets NULL there.

Query
QuarterlySales := [
| region | quarter | revenue |
|--------|---------|---------|
| East   | q1      | 100     |
| East   | q2      | 120     |
| West   | q1      | 80      |
];

query { PIVOT revenue BY quarter PER region (QuarterlySales) };
Result
 region  q1   q2
 ──────  ───  ────
 East    100   120
 West     80  NULL
(2 rows)

The column headers come from the data, so the output schema is open — downstream operators that need named columns may need a ρ. UNPIVOT is the reverse, folding listed columns back into rows.

Recipe 2: many rows to a list (COLLECT), and back (μ)

COLLECT, inside a γ, gathers a group's values into an array instead of reducing them to one number:

Query
Orders := [
| customer | order_id |
|----------|----------|
| Ann      | 1        |
| Ann      | 2        |
| Bo       | 3        |
];

Gathered := { γ customer, COLLECT(order_id) → order_ids (Orders) };
query { Gathered };
Result
 customer  order_ids
 ────────  ─────────
 Ann       [1, 2]
 Bo        [3]
(2 rows)

μ (unnest) is the exact inverse — it explodes an array into one row per element, and WITH ORDINALITY records each element's position:

Query
query { μ order_ids WITH ORDINALITY pos (Gathered) };
Result
 customer  order_ids  pos
 ────────  ─────────  ───
 Ann               1    1
 Ann               2    2
 Bo                3    1
(3 rows)

μ is also how you flatten a source that already stores an array (a JSON field of line items, say) into flat rows.

Recipe 3: parent pointers to a tree (TREE)

An adjacency table — each row pointing at its parent — folds into a forest of nested documents in one pass. TREE key BY parentKey follows the edge to any depth; a row whose parent is absent (or NULL) is a root.

Query
Employees := [
| id | manager_id | name |
|----|------------|------|
| 1  | 0          | Ada  |
| 2  | 1          | Bob  |
| 3  | 1          | Cara |
| 4  | 2          | Dan  |
];

query { TREE id BY manager_id ORDER id ASC AS reports (Employees) };
Result
 id  manager_id  name  reports
 ──  ──────────  ────  ──────────────────────────────
  1           0  Ada   [{id: 2, manager_id: 1, name:…
(1 row)

One row per root, each carrying its whole subtree in the added column — the recursive version of COLLECT, which nests a single level. From there it is ordinary nested data: μ reports walks one level, π reports reads the column.

Variations

Pitfalls

Check it