> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Materialized Views

> Store a view's output and keep it current in the same transaction as every write

Views are **virtual** by default: Bijection evaluates the expression each time
you read the view. Calling `.materialize()` on a view asks Bijection to store its
output and maintain it instead:

```ts bijection/schema.ts {12} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
customer_summaries: defineView({
  key: { from: "customers", field: "_id" },
  expression: q
    .table("customers")
    .as("customer")
    .leftJoin(orderTotals, { left: "customer._id", right: "totals.customer_id" })
    .select({
      name: q.field("customer.name"),
      total_cents: q.field("totals.total_cents"),
    }),
})
  .materialize()
  .index("by_total", ["total_cents"]),
```

The definition, the results and the way you read the view stay the same. Only
the execution strategy changes.

## Virtual and materialized

| | Virtual (default) | Materialized |
| - | - | - |
| Declared with | `defineView({ ... })` | `defineView({ ... }).materialize()` |
| Work happens | When the view is read | When a mutation changes an input |
| Reads | Evaluate the needed part of the expression | Read the stored output |
| Index reads on computed fields | Evaluate and sort within the view's limits | Read an index range of the stored output |
| Results | Current as of the read | Current as of the read |
| Readable from queries and mutations | Yes | Yes |

A materialized view is never stale. There's no refresh to schedule and no lag
to monitor: a read always returns the same result the virtual view would
return at the same snapshot, including the pending writes of the current
mutation.

## How materialized views stay current

When a mutation writes to a table that a materialized view reads, Bijection
recomputes the affected view rows as part of that mutation's transaction. The
change to the table and the change to the view commit together, or not at all.

For `customer_summaries`, inserting an order recomputes that one customer's
row. Moving an order from one customer to another recomputes both customers,
using indexes to find them. A small change doesn't rescan every customer.

That work is bounded by the view's [work limits](/views/defining-views#work-limits).
If a write would require more maintenance than the limits allow, the mutation
fails. Bijection doesn't fall back to a stale result or to finishing the work
later.

<Warning>
  A mutation that changes many inputs of a materialized view at once, such as
  one that updates every order, does all the matching view maintenance in the
  same transaction. Keep such mutations small, or split them across several
  mutations, the same way you would to stay within
  [transaction limits](/database/writing-data#write-performance-and-limits).
</Warning>

## Index requirements

A materialized view needs indexes that let Bijection find affected rows in both
directions:

* **Joins** need the index on the right side, as for any view, and also an
  index on the left table that begins with the `left` fields. When a joined row
  changes, that index finds the view rows that referenced it. A join on the left
  table's `_id` uses the built-in `by_id` index.
* **Grouped inputs** need an index that begins with the grouping fields, as for
  any view.

In the example above, the join is on `customer._id`, and `orders` has an index
on `customer_id` for `orderTotals`, so no extra index is needed. Bijection
refuses to maintain a materialized join whose left fields have no such index,
with an error naming the table and fields.

Views over views can be materialized too. A materialized view can read a virtual
view, and the other way around.

## Choosing between them

Start with a virtual view. It has no storage and no write-time cost, and it's a
good fit when:

* Reads are narrow, such as `db.get` on one key or an index range over fields
  the view passes through from its source table.
* The view is small enough to evaluate within its limits on every read.

Materialize a view when:

* You order or filter by a computed or joined field, such as a total, across
  many rows. The stored output can be read by index instead of evaluated and
  sorted.
* Many clients read the view much more often than its inputs change.

Because both forms return the same results, the functions that read a view are
the same whether or not it's materialized.

<Note>
  Materializing a view stores its output as a derived copy of its inputs. It
  doesn't create a new source of truth: the tables it reads remain the only
  place its data is written, and access to the view is still checked on every
  read. See [Access rules](/access/overview).
</Note>
