Stored vs Non-Stored Computed Fields, and the Search Trap

store=True is one keyword. It is also, more than any single other decision, what determines whether an Odoo schema stays fast at scale, or quietly rots into a system nobody trusts to report from.

Computed fields are where Odoo schemas go right or wrong. The decision looks trivial (add store=True or don't) and it is made dozens of times across a project by whoever is writing the field that day. Each decision is individually small. Together they decide whether the system stays responsive at a million records or grinds.

What the two modes actually cost

A non-stored computed field has no database column. Its value is calculated, in Python, every single time the field is read. Nothing is persisted; nothing is stale.

A stored computed field (store=True) has a real column. The value is written there, and recomputed only when something in its @api.depends declaration changes. Reads are then ordinary column reads.

Neither is "the fast option." Each is fast in one direction and expensive in the other, and the whole skill is matching the mode to how the field is actually used.

The non-stored failure mode: death by list view

Put a non-stored computed field in a list view and every page render computes it once per visible row. If the computation itself runs queries, and computations on related data usually do, a hundred-row list becomes a hundred Python computations and several hundred queries, on every page load, every time. The field works perfectly in a form view showing one record and falls over in a list showing a hundred. The fix is store=True with a correct depends.

The stored failure mode: recomputation storms

Storing has the opposite danger. When any field named in a stored field's @api.depends chain changes, every record whose computed value depends on it is recomputed and rewritten. Declare a stored computed field on a high-write model with a dependency that fans out widely, and a single ordinary save triggers a cascade of recomputations across thousands of rows. That turns a fast write into a slow one and inflates every transaction that touches the dependency.

This is write amplification, and it is invisible in development. With fifty test records the storm is fifty cheap recomputations. In production it is the reason saving a sale order takes four seconds.

The search trap

Here is the part that surprises people. A non-stored computed field has no column, so the database cannot search or sort on it. If you put a non-stored computed field in a list view, the column renders fine, but clicking its header to sort silently does nothing, and it cannot appear in a filter. The field looks like a first-class field and behaves like one in every way except the two that list views are for.

You can rescue searchability by giving the field a search method: a function that translates a search on the computed value into a domain on real columns. But a search method is only as good as the expression you can write, and it cannot make SQL sort on a value that exists only in Python. The honest rule: if users need to sort or filter by a field, it almost certainly needs to be stored.

The depends graph is the real design

For any stored computed field, the design is not the compute function. It is the @api.depends declaration. Two questions decide whether the field is well-behaved:

  • Is the dependency complete? A missing dependency means a stale value: the field shows the wrong number until something unrelated forces a recompute. These bugs are miserable because the value is sometimes right.
  • Is the dependency narrow? An over-broad dependency, one that names a field changing constantly or reaches across a large relation, is what produces recomputation storms. Depend on the least that is still correct.

How we decide

The working rule we apply on every schema:

  • Read in list views, or needs sort or filter: store it.
  • Read only in a form, one record at a time, and cheap to compute: leave it non-stored.
  • Expensive to compute and its dependencies change often: neither answer is free. Reconsider whether it should be a computed field at all, or a value updated explicitly by the process that owns it.
  • Stored: audit the depends graph and ask, for each entry, how many records recompute when it changes.

store=True is one keyword. Treat it as a one-keyword decision and a schema accumulates, field by field, the conditions for being slow. Treat it as the architectural choice it is, and the same schema stays fast at a scale the early version never saw.