Skip to main content

The pipe operator

The pipe operator | takes whatever's on the left and passes it as the first argument to whatever's on the right. It's the syntax that makes AQL feel composable instead of nested.

// These two are equivalent:
users | avg(users.age)
avg(users, users.age)
Try it interactively

Practice in the AQL Playground: Pipe (1) · Pipe (2).

Why it matters

AQL is composable by design: every function takes input, produces output, and that output can feed straight into the next function. You don't strictly need pipe for that. sum(filter(orders, orders.country = 'Singapore'), orders.total_value) works fine. But it reads inside-out, and three or four steps deep it stops being readable.

Pipe is what makes that composability ergonomic. SQL forces one shape (SELECT ... FROM ... WHERE ... GROUP BY ...); AQL lets you break a query into small steps and chain them, so the code reads in the same order you think about it: start with a table, narrow it down, aggregate. The pipe is the signal that AQL is built to be read left-to-right, step by step.

Example

Say you want total order value for Singapore. You need two steps:

  • filter(): keep only Singapore orders.
  • sum(): add up the totals.

With pipes:

orders
| filter(orders.country = 'Singapore')
| sum(orders.total_value)

Read it top to bottom: take orders, keep only Singapore, sum the totals. Each pipe feeds its left side into the next function's first argument.

Mental model

Each step that takes a table walks it one row at a time. filter(orders.country = 'Singapore') checks the condition on each row; sum(orders.total_value) adds the value column row by row. Whenever you write an expression like orders.country or orders.quantity * orders.price, it's evaluated against whichever row the step is currently looking at (the current row). This per-row evaluation is the foundation that filter, group, and aggregate all build on.

Tables in, scalars out

Every AQL expression produces either a table (rows you can keep filtering or grouping) or a scalar (a single value, the end of a chain). The pipe respects that:

  • orders → a table. You can keep piping.
  • orders | filter(...) → still a table. Keep piping.
  • orders | sum(orders.total_value) → a scalar. The chain ends. There's nothing left to filter.

If a step expects a table and you hand it a scalar (or vice versa), the chain breaks. Once you've internalized "what shape is this step's output?", reading AQL gets a lot easier.

When pipes shine

  • Long chains (3+ steps): readable top-to-bottom instead of nested.
  • Reusable fragments: users | filter(users.is_active) can be a building block.
  • Debugging: comment out a single line to see the intermediate result.

For the full signature and edge cases, see the pipe reference.

Next

Filtering: where() vs filter(), and when each one applies.


Open Markdown
Let us know what you think about this document :)