---
title: "Selecting rows — quario"
description: "Choosing, filtering, ordering and ranking the rows a quario report walks, and the order those steps run in."
url: "https://getquario.com/docs/diving-deeper/selecting-rows"
---

# Selecting rows

## Intent

A JSONPath selects the record set, and filtering, ordering, and ranking are
separate structured steps on top. Four keys do the work, and the order they run
in decides what every aggregate in the document means.

## Design decisions

**`data` is a path, never an expression.** A `=` in `data` is a literal
character. The one selection mechanism is JSONPath, so the shape of the query
stays analysable and the engine can bound it.

**`where` sees one row, so it can’t rank.** It’s a per-row boolean and
nothing more. Ranking is `take`, which is the only key that can see the set.

**A filter selector reaches where `where` can’t.** A selector sits at any
segment of the path, including segments above the row set. That’s how a
report selects rows by a property of their container.

**A filter selector and `where` don’t mean the same thing by the same words.**
RFC 9535 reads a bare relative query as an existence test, while `where` is an
ordinary boolean and tests truthiness.

**`take` runs after the sort, and a path slice runs before it.** A slice is
selection, so it picks rows by their position in the input. A negative step
can reverse that order, but never ranks it. That’s why a slice isn’t a top-N.

**Rows `take` drops leave the aggregates too.** That’s the same removal
`where` performs, and it’s what separates `take` from `visible`, which is
presentation and keeps its rows in the totals.

**A group ranks its instances apart from their rows.** A group’s `sort` and
`take` work inside each instance. `instances` orders and cuts the instances
themselves, so “the five largest regions” is one declaration rather than a
pre-sorted query.

**An instance `instances.take` drops still counts in the totals around it.**
It never renders, but the group aggregates around it and the report aggregates
include its rows, as they include a row a group’s `take` drops. A top five of
regions therefore reads against the grand total of every region.

**The query carries a traversal budget by default.** Render input may be
untrusted and unbounded descent would exhaust the call stack, so `maxDepth`
defaults to 500 rather than to nothing.

## API walkthrough

### Choosing the record set

**`data`** is an RFC 9535 JSONPath selecting the row array from your input.

```json title="selection"
{ "data": "$.orders[*]" }
```

A path may carry a **filter selector**, which is JSONPath’s own expression
dialect rather than quario’s.

```json title="filtering the containment path"
{ "data": "$.regions[?@.active == true].orders[*]" }
```

That takes the orders of active regions only. No `where` expresses it, because
`@` binds to the order and an order carries no reference to its region.

Write the comparison out. `[?@.active]` keeps a region whose `active` is
`false`, because the member exists, where `where: "=@.active"` would drop it.

Selectors may use `match(value, pattern)` for a full match and `search(value,
pattern)` for a substring match. Patterns follow bounded RFC 9485 I-Regexp
semantics, not JavaScript `RegExp`, so `\d`, lookarounds, and backreferences
fall outside it. The compiler checks a pattern written as a literal. A pattern
arriving from data produces no match at render rather than throwing.

### Dropping rows

**`where`** is a boolean evaluated per row with `@` bound to that row. The
report drops rows returning falsy. It’s report-level only.

```json title="filtering rows"
{ "where": "=@.status != 'void'" }
```

A report aggregate read inside `where` is `null`, because `where` runs before
the aggregates exist. There is no earlier moment that could supply one.

### Ordering

**`sort`** takes keys applied left to right. The first key is the primary
key. `dir` defaults to `"asc"`, and the sort is stable, so rows whose keys
compare equal keep the order they arrived in.

```json title="ordering"
{ "sort": [{ "by": "=@.region" }, { "by": "=@.amount", "dir": "desc" }] }
```

Keys compare with JavaScript ordering, so a mixed-type or nullish key orders by
that ordering. Keep a key one type where the order matters.

A `sort` on the report orders the whole set. A `sort` on a group orders the
rows within each instance.

A report aggregate read in a report sort key is `null`, for the same reason it’s
`null` in `where`. A group’s sort key is the one exception. The engine computes a
group instance’s aggregates over its partition before it orders the rows, so
`=region.subtotal - @.amount` works. Declaring `take` on that group withdraws
the exception, since ranking has to happen first.

### Ranking

**`take`** keeps the first N rows after that node’s sort. It’s a literal
integer of one or more.

```json title="a top five"
{ "sort": [{ "by": "=@.amount", "dir": "desc" }], "take": 5 }
```

Sort descending and take N for a top-N, ascending for a bottom-N. Without a
sort it keeps the first N as they arrived, and a count larger than the set
keeps every row.

A `take` on the report ranks the whole set. A `take` on a group ranks within
each instance.

To show five rows and still total over all of them, use `visible` with a count
runner instead. Suppressed rows stay in the aggregates.

### Ranking the instances

**`instances`** sits on a group beside its `sort` and `take`, and holds an
optional `sort` and `take` of its own.

```json title="the five largest regions"
{
  "name": "region",
  "by": "=@.region",
  "aggregates": { "subtotal": "sum:=@.amount" },
  "instances": { "sort": [{ "by": "=region.subtotal", "dir": "desc" }], "take": 5 }
}
```

An `instances.sort` key reads what a group band reads. That’s the group’s own
handle, the handles of the groups around it, and `$`. `@` is unbound, so
`@.amount` throws there. `run` is unbound too. Without `instances.sort`, instances appear in
the order their first row arrived, and keys that compare equal keep that order.
`instances.take` keeps the first N in the resulting order. A hollow instance
still uses one of the N places, because the cut happens before anything
renders.

### The order of operations

Select with `data`, drop with `where`, apply the report `sort`, apply the
report `take`, then partition into groups, each group applying its own `sort`
then `take`. Each group then orders and cuts its instances by `instances`. An
`instances.sort` key therefore sees the aggregates of the rows each instance
keeps.

Aggregates and running totals cover the rows that remain, in the resulting
render order, so a running total follows what the reader sees.

### Bounding the traversal

Budgets are instance options, never schema fields.

```ts title="report.ts"
import { quario } from 'quario'

const q = quario({ query: { maxDepth: 64, maxNodes: 10_000, maxResults: 1_000 } })
```

An explicit `maxDepth` replaces the default of 500, and `Infinity` disables a
budget. Exceeding one throws a located `RangeError` carrying `code`,
`limit` and `actual`.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"Selecting rows","item":"https://getquario.com/docs/diving-deeper/selecting-rows"}]}
```
