Skip to Content

When to Store a Computed Field (and What Storing It Costs)

Non-stored computed fields are the right default right up until they aren't. Here is how to recognize the line and what you owe the database when you cross it.
August 24, 2026 by
When to Store a Computed Field (and What Storing It Costs)
DimeSoft Business Solutions, Inc., Bill Dimes

Odoo computed fields default to non-stored, and for good reason. Non-stored means the value is calculated on the fly, lives nowhere in the database, and costs nothing to maintain between reads. It is the right default for fields that are cheap to compute, read infrequently, and never used in search domains or list-view sort orders.

The problem is that the docs leave it there. They describe the default, show the syntax, and move on. What they skip is the part where your product manager asks why the custom margin field won't sort in the list view, or your operations team notices their weekly export takes eleven minutes because every record is being recomputed at export time. Those are the moments that force a switch to store=True, and that switch is not free.

This article is about recognizing the signals that force the switch and understanding what you owe the database when you make it. In an earlier article on extending Odoo models versus building alongside them, we used an unstored computed field deliberately, a computed summary of a related model's state that had no business being stored. That example quietly invites this follow-up: how do you know when storing is the right call, and what does it actually cost?

The signals that force a switch to stored

Four patterns, in the order they tend to surface.

List-view sorting and filtering. This is the most common trigger. A user opens a list view, clicks the column header for your computed field, and nothing happens, or the sort order is wrong and inexplicable. That behavior is not a bug. Odoo sorts list views by issuing an ORDER BY clause against the database. A non-stored computed field has no database column to sort by. The ORM has no clean path to sort on it, so it either ignores the sort or applies it only to the current page, which looks broken to the user. Filtering has the same problem: a search domain against a non-stored field forces a full-table read and Python-side evaluation, which scales poorly and is often silently disabled by the ORM for large datasets.

Search domain performance. If your computed field appears in a search domain that runs against a large table, non-stored means recomputing every record in the table to evaluate the filter. For a table with a few hundred records, this is annoying. For a table with tens of thousands, it is a support ticket. The moment a computed field becomes a filter target on a large table, it needs a database column behind it.

Reporting, exports, and cross-record aggregation. Odoo's reporting layer and the built-in export pull data from the database. Non-stored computeds require recomputation for every record in the result set, serially, inside the ORM. A report covering 5,000 sale orders that each compute margin from three related fields is not a report; it is a batch job. If the field needs to appear in a pivot view, a graph view, or a scheduled export, store it.

Use as a group_by target. Group-by operations in list and pivot views require a real database column. There is no workaround for this one. If the field will ever be a grouping dimension, it must be stored.

What storing actually costs

This is the conversation the docs skip. store=True is not a performance optimization. It is a trade: you pay write-time cost to avoid read-time cost. Understanding what you are paying matters before you make the trade.

Recomputation triggers and the depends list

When you store a computed field, Odoo uses the @api.depends() declaration to decide when to recompute and write the stored value. If the depends list is incomplete, the stored value goes stale silently. There is no error, no warning. The column in the database simply holds the wrong answer, and the only way to notice is when someone looks at the data carefully enough to catch the discrepancy.

This is the most dangerous failure mode with stored computed fields. Write the full depends list before you write the compute method. Not after, not during: before. Think through every field path that, if changed, should update the stored value. For a margin field on sale.order.line, that probably includes the unit price, the product cost, the quantity, and possibly a discount field. It might include fields on the related product or pricelist. Missing any one of them means the stored margin is wrong every time one of those fields changes without the others.

Depends paths through relational fields (using dot notation like "order_id.currency_id") require particular care. The ORM has to traverse the relation to find records to invalidate. If the path is deep or the related model is large, the invalidation query itself becomes expensive.

Bulk write performance

Every time a dependency field changes, Odoo recomputes and writes the stored value for every affected record. On single-record edits through the UI, this is invisible. On bulk operations, it surfaces immediately.

Consider a product cost update that touches 800 products. If you have a stored computed margin on sale.order.line that depends on product cost, that one update fans out to recompute and write margin on every open sale order line that references those products. In a busy distribution environment, that could be tens of thousands of writes triggered by a single product import. If you have not tested the field under bulk conditions in development, you will find out about this fan-out in production at the worst possible time.

The discipline: before you ship a stored computed field, run a bulk update against its dependencies in a development environment with realistic data volumes. Watch the query log. If the recompute takes thirty seconds on dev data, estimate what it does on production data before you deploy.

Database index considerations

A stored computed field gets a database column, but it does not automatically get an index. If the field appears in a search domain, a WHERE clause without an index means a sequential scan of the table. For small tables this is fine. For large tables it is not.

When a stored computed field becomes a search or filter target on a table with significant row counts, add an index. In Odoo you do this by setting index=True on the field declaration. This is one line of code with real impact on query performance, and it is easy to forget because the field works without it, just slowly.

class DsSaleOrderLine(models.Model):
    _inherit = "sale.order.line"

    # Stored and indexed: this field is filtered and sorted in list views
    # and appears in margin reports. Both uses require a real DB column.
    # The depends list covers every field path that affects margin.
    ds_margin_pct = fields.Float(
        string="Margin %",
        compute="_ds_compute_margin_pct",
        store=True,
        index=True,  # search target: needs the index
    )

    @api.depends(
        "price_unit",
        "product_uom_qty",
        "discount",
        "product_id",
        "product_id.standard_price",
    )
    def _ds_compute_margin_pct(self):
        for line in self:
            cost = line.product_id.standard_price or 0.0
            revenue = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
            if revenue:
                line.ds_margin_pct = (revenue - cost) / revenue * 100.0
            else:
                line.ds_margin_pct = 0.0

The depends list is explicit and complete. The index is declared alongside the store. These are not afterthoughts.

The readonly trap: force_save on form views

There is one more gotcha that the docs do not flag clearly, and we have hit it enough times that it deserves its own section.

When a stored computed field is also marked readonly on a form view, Odoo's web client will not submit the field value when the record is saved. The field is read-only, so the client assumes there is nothing to send. The ORM agrees. The result: the value displays correctly on screen, but the database column never receives the write. You have a stored computed field that is not actually stored, and it will look correct right up until the record is reloaded from a cold cache.

The fix is one attribute in the view XML: force_save="1" on the field definition.

<!-- Without force_save, a readonly stored computed field will not
     write its value to the database on save. The display looks correct
     but the column stays stale. -->
<field name="ds_margin_pct" readonly="1" force_save="1"/>

<!-- Another common case: a boolean flag computed and stored,
     shown on a document form in read-only context. -->
<field name="ds_is_online_direct_import_document" force_save="1"/>

force_save="1" tells the web client to include the field value in the save payload even though the field is read-only. The ORM then writes the value to the database as it normally would for any stored field.

This is easy to miss because the symptom is subtle. The field looks right immediately after an edit that triggers a recompute, because the recomputed value is held in memory. The stale column only surfaces after a page reload, a background cron, or a report that reads directly from the database. By then the connection to the missing force_save is not obvious. If you are storing a computed field and marking it read-only in a form view, add force_save="1" at the same time you write the field declaration. Do not wait for the bug report.

The discipline of storing cleanly

Four practices. Follow all four or skip store=True until you can.

Write the depends list first, completely. Before the compute method exists, trace every field path that should invalidate the stored value. Write the @api.depends() decorator from that list. Then write the compute. If you write the compute first, you will miss dependencies because your mental model is in the happy path, not in the invalidation paths. Incomplete depends declarations cause silent stale data, which is harder to diagnose than an outright bug.

Exercise the field with bulk updates in development. Create or import a realistic volume of records. Run a bulk update against the dependency fields. Watch wall-clock time and the database log. If the recompute is slow at test volumes, it will be slow in production. Find out in development, not during a live product import.

Add the index when the field is a search target. If the field will appear in a search domain, a filter bar, or a group_by clause, it needs an index. Add it at declaration time. Do not wait until someone files a slow-report ticket.

Add force_save="1" when the field is read-only on a form view. If the field is stored and displayed as read-only, the web client will not submit the value on save without this attribute. Add it at the same time you write the field. It is one attribute and it prevents a class of bug that is genuinely hard to trace after the fact.

When stored computed fields are not enough

There is a failure mode beyond stale data and slow recomputes: stored computed fields that are simply too expensive to keep current through normal ORM recomputation, because the dependency fan-out is too large or the compute method is too slow even when correct.

When this happens, the answer is not to optimize the compute method further. The answer is to step outside the computed-field mechanism entirely and update the column through explicit ORM hooks: write(), create(), or an automated action triggered at specific points in the workflow rather than on every dependency change. This is closer to classical denormalization: you accept that the value is not always instantly current and control exactly when it updates, rather than letting the ORM's dependency graph decide.

This is the harder path. It requires more code, more testing, and more discipline to maintain. But it is the right path when the ORM's dependency mechanism cannot keep up with your data volume. The computed-field mechanism is a convenience, not a constraint. When it stops being convenient, use something else.


Slow list views on custom fields and stale values in stored computeds are both fixable. They are also both patterns we see regularly on deployments we audit: either someone stored a field without indexing it, someone left a depends list incomplete and the data drifted, or a read-only stored field was never given force_save and the column quietly stayed empty. None of these take long to diagnose once you know what you are looking for.

DimeSoft does this kind of analysis as part of code and performance audits. If any of these patterns sounds familiar in your Odoo deployment, a direct conversation about what you are seeing is the right place to start. We will tell you honestly what we find and what it would take to fix it. Reach out if that would help.

The Diagnostic-Before-Fix Discipline: How We Debug Odoo
Most bad Odoo fixes look correct in code review and break something else in production. The reason is almost always that the fix preceded the diagnosis.