Skip to main content
A view can combine several tables. Joins attach matching rows from another table, and grouping reduces many rows to one row per group, which you then join back to the view’s key table. This view shows each order next to the name of its customer:
bijection/schema.ts

Joins

.leftJoin(right, options) and .innerJoin(right, options) match each row of the current relation with rows of right whose fields are equal: The two kinds differ only when there’s no match:
  • leftJoin keeps the left row. The right side’s fields are optional in the output type, so customer_name above is string | undefined.
  • innerJoin drops the left row.

The right side

The right side of a join is either:
  1. A table, written q.table(...), optionally with .as() and .filter(). Name an index on that table with index. The index must begin with the right field. To join on the right table’s _id, use the built-in "by_id" index.
  2. A grouped relation, written q.table(...).groupBy(...).aggregate(...), optionally with .as(). Don’t pass index. See Grouping.
A right side built with .select() or with another join isn’t supported. Give both sides an alias with .as(). A join combines the fields of both rows, and Bijection refuses a join whose two sides have a field with the same name.

Cardinality

A view has one row per key, so joins in a view’s main chain use cardinality: "one": at most one right row may match each left row. Bijection checks this as it reads. If a second row matches, the read fails with a ViewCardinalityViolation error rather than returning two rows for one key. Make sure the data can’t have two matches:
  • Join on the right table’s _id, as order_details does.
  • Join a link type on an endpoint whose maximum is 1.
  • Join a grouped relation on its grouping key, which has one row per group.
A cardinality: "many" join in the main chain would produce several rows for one key, so Bijection refuses that view when you deploy.

Grouping

.groupBy(fields).aggregate({ ... }) returns one row per distinct combination of the grouping fields, with one output field per aggregate:
bijection/schema.ts
The grouped row contains the grouping fields and the aggregates: here customer_id, order_count and total_cents. A grouping field given as a path, such as "order.customer_id", is named after its last segment. q.sum requires the field to be declared as v.int64() or v.number(), and every row in the group to have a finite value for it. Counts are exact integers, and so are sums of v.int64() fields.

Joining a grouped relation

A grouped relation doesn’t have a key of its own, so it can’t be a view by itself. Join it to the view’s key table on its complete grouping key, in the order you grouped by:
bijection/schema.ts
The table you group needs an index that begins with the grouping fields. Here orders has by_customer on ["customer_id"], which Bijection uses to read one customer’s orders without scanning the table.
A group with no rows has no grouped row. A customer without orders therefore has no match in orderTotals, and with leftJoin its order_count is undefined, not 0n.

Missing and null values

A missing field and a field set to null are different values for grouping and joins. Rows where region is missing form one group, rows where region is null form another, and each only matches rows with the same kind of value.

Compound join keys

Compound join keys are in beta.
left and right can each be an array of one to three distinct fields, with the same length on both sides. A row matches when every pair of fields is equal:
bijection/schema.ts
The same rules apply as for a single field. An index on a right table must begin with all the right fields in order. A grouped right side must be joined on its complete grouping key, in the order of groupBy; a prefix of the key isn’t enough to guarantee one match. Here sales needs an index beginning with ["region", "product_code"].

Exact decimal and distribution aggregates

Exact decimal and distribution aggregates are in beta.
These aggregates add exact decimal arithmetic and distribution statistics to aggregate:
bijection/schema.ts
  • Decimals. decimalSum and decimalPercentile read a field declared as v.string() holding decimal text such as "1250.40". Values are never converted to floating point. scale is an integer from 0 to 38. A value that would need rounding to fit scale, a value that isn’t a decimal, and a result with more than 38 digits fail the read with an InvalidDecimal or DecimalOverflow error.
  • Percentiles. basisPoints is an integer from 0 to 10,000, where 5,000 is the median. The result is the first observed value at which the cumulative share of rows reaches the requested fraction, and 0 selects the minimum. It never interpolates between values or approximates. percentile requires a v.int64() or v.number() field.
  • Distinct counts. A missing value and null count as two different values.
countDistinct and the percentiles keep the group’s values while they’re computed, and those bytes count against the view’s max_bytes limit.