skip to content

In Django, what does save(update_fields=[...]) change about the UPDATE statement, and which surprises come with it?

level: middleimportance: should knowfreq 45%

answer

  1. only the named columns
  2. empty list means no query
  3. auto_now needs to be listed
  4. a row that is not there

basics

~20 s

save(update_fields=[...]) writes only the named columns in an UPDATE, so other columns changed elsewhere are not overwritten. It forces an update, skips auto_now fields and derived fields not listed, and raises NotUpdated if no row matches.

solid answer

~40 s

With `update_fields`, `save()` issues an `UPDATE` that sets only those columns, instead of rewriting every field from the in-memory instance. That is smaller, and it avoids clobbering columns another request changed since you loaded the row. The surprises: it forces an update, so if no row matches Django raises `Model.NotUpdated` (since 6.0; earlier a generic `DatabaseError`); an empty iterable skips the save entirely; unknown, many-to-many or non-concrete names raise `ValueError`; only the listed fields' `pre_save()` runs, so `auto_now` timestamps are not refreshed unless listed; and a value your `save()` override derives, like a slug, is lost unless you add it. Instances loaded with `only()` or `defer()` get an automatic `update_fields` of the loaded fields.

code

python · 14 lines
python
page = WikiPage.objects.get(pk=7)
page.title = "Installing on Linux"

# Writes only the title column; body edits made elsewhere survive
page.save(update_fields=["title"])

# auto_now field updated_at is NOT refreshed by the line above;
# list it when the timestamp should move
page.save(update_fields=["title", "updated_at"])

try:
    WikiPage(pk=999, title="Ghost").save(update_fields=["title"])
except WikiPage.NotUpdated:
    pass  # no row with pk 999, so nothing was updated

go deeper

for a junior

Recall that update_fields limits which columns save() writes, and that the fields you name must exist on the model.

for a middle

Explain the empty-list, auto_now, deferred-loading and missing-row behaviours, and how a save() override must extend update_fields.

for a senior

Use update_fields to stop long-running code from clobbering concurrent edits, and know it does not settle races on the same column.

for a principal

Decide where partial writes are the norm, for example tasks and hot rows, and where whole-object saves keep code simpler.

## What a plain save() writes By default, `instance.save()` on an existing row writes **every concrete field** of the model: Django builds an `UPDATE` that sets each column to the value currently held on the Python instance. Django does not track which attributes you changed. If you loaded a wiki page, changed its `title`, and saved, the `body` column is also rewritten with whatever the instance held when it was loaded. ## What update_fields changes `page.save(update_fields=["title"])` tells Django to set **only** the listed columns: ```sql UPDATE "wiki_wikipage" SET "title" = 'Installing on Linux' WHERE "id" = 7; ``` Two benefits follow: - **Smaller writes**, which matters for wide rows and large text columns. - **No clobbering of other columns.** If another request edited `body` after you loaded the page, a full save would overwrite that edit with your stale copy; saving only `title` leaves it alone. It does **not** protect the same column from concurrent edits: two requests saving `title` still race, and the last one wins. ## The rules and surprises | Situation | Behaviour | |---|---| | `update_fields=[]` (empty iterable) | the save is skipped; no query runs | | `update_fields=None` | a normal save of all fields | | a name that is not a concrete field, or is a many-to-many | `ValueError` naming the bad fields | | no row matches the primary key | `Model.NotUpdated` (Django 6.0+), "Save with update_fields did not affect any rows." | | a field with `auto_now=True` not in the list | not updated, because only listed fields' `pre_save()` runs | | a field your `save()` override derives, not in the list | computed in memory, never written | | instance loaded with `only()` / `defer()` | Django uses the loaded fields as an automatic `update_fields`, adding any deferred field you assign | | used together with `force_insert` | not allowed; `update_fields` always forces an update | ## Overrides must cooperate A `save()` override that recomputes a derived value has to include it when the caller narrowed the write. Django's own docs show the pattern for a slug: ```python def save(self, **kwargs): self.slug = slugify(self.title) if (update_fields := kwargs.get("update_fields")) is not None and "title" in update_fields: kwargs["update_fields"] = {"slug"}.union(update_fields) super().save(**kwargs) ``` The same applies to an `updated_at = DateTimeField(auto_now=True)`: code that saves with `update_fields=["title"]` leaves the timestamp untouched unless `updated_at` is listed too. ## When to use it 1. **Targeted edits in views or tasks**, such as marking a page as reviewed, where only one or two columns should change. 2. **Long-running jobs** that hold instances for a while and must not overwrite unrelated edits made meanwhile. 3. **Counters or flags on hot rows**, combined with an `F()` expression for the value itself, so the database computes it. Avoid it where the full object was edited as a whole, such as a `ModelForm` covering every field; listing fields there only adds a way to forget one. ## update_fields versus QuerySet.update() Both write a subset of columns, but they are different tools: | | `instance.save(update_fields=[...])` | `QuerySet.filter(...).update(...)` | |---|---|---| | Rows | one instance | any number of rows in one statement | | Runs your `save()` override | yes | no | | Needs a loaded instance | yes | no | | Can use `F()` expressions | yes | yes | | Instance in memory afterwards | holds the saved values | unchanged, and now stale | Choose `update_fields` when the per-instance logic in `save()` must run; choose `update()` for bulk changes where it need not. ## Diagnosing a missing write A classic bug report reads "the page title changed but the slug and the last-edited time did not". The cause is almost always a caller passing `update_fields=["title"]` while the model derives `slug` in `save()` and stamps `updated_at` with `auto_now`. The fix is either widening the caller's list or making the override add its derived fields, and a test that saves with `update_fields` and reloads the row.

  • What does Django write when you save an instance loaded with WikiPage.objects.only("title")?
    Django treats the loaded fields as an automatic `update_fields`, so `save()` writes only `title` (plus any deferred field you assigned a value to). Columns that were never loaded are not overwritten with placeholder values, which is what makes saving a deferred instance safe.

saying these in an interview costs you the question

  • update_fields prevents two requests from racing on the same column.
  • auto_now fields are always refreshed, even when not in update_fields.
  • An empty update_fields list saves every field.
  • update_fields quietly inserts the row if it does not exist yet.
  • Django tracks changed attributes and writes only those by default.