A Django admin course form hangs because its instructor select lists every user; how do autocomplete_fields and raw_id_fields fix it, and how do they differ?
answer
- the default widget loads every row
- search as you type
- the related admin must be searchable
- a primary key and a magnifying glass
basics
~20 sThe default ForeignKey widget is a select filled with every related row. autocomplete_fields swaps in a search-as-you-type box that queries the related ModelAdmin's search; raw_id_fields swaps in a plain id input with a lookup popup.
solid answer
~40 sFor a `ForeignKey` or `ManyToManyField`, the admin renders a `<select>` containing **every** related object, built from each row's `__str__()`. With 200,000 users that is a huge query and page, repeated in every inline row. `autocomplete_fields = ["instructor"]` replaces it with a Select2 box that fetches matches as you type from the admin's autocomplete view. That view uses the **related** `ModelAdmin`: it requires that model to be registered with `search_fields` (system checks `admin.E039`/`admin.E040`), applies its `get_queryset()`, `get_search_results()`, ordering and paginator, honours `limit_choices_to`, and requires view or change permission on the related model. `raw_id_fields = ["instructor"]` shows a text input holding the primary key (comma-separated ids for many-to-many) and a magnifying-glass popup of the related change list. Autocomplete is friendlier; raw id needs no search configuration and shows ids rather than names.
code
python · 17 linesfrom django.contrib import admin
from courses.models import Course, Lesson
class LessonInline(admin.TabularInline):
model = Lesson
raw_id_fields = ["reviewer"] # ops staff paste user ids
@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
autocomplete_fields = ["instructor", "co_instructors"]
inlines = [LessonInline]
# The user model's registered ModelAdmin must define search_fields
# (django.contrib.auth's UserAdmin does), or check admin.E040 fails.go deeper
Recall that the default related-field widget lists every row, and that autocomplete_fields and raw_id_fields replace it.
Explain autocomplete's requirements (registered related admin with search_fields), what raw id shows, and why inlines multiply the cost.
Show you diagnose slow change forms by widget, scope autocomplete via the related admin's get_queryset and permissions, and index searched columns.
Decide how much of a huge related table editors should reach from a form, and whether selection belongs in a dedicated tool.
## Why the default widget breaks For a `ForeignKey`, the admin's form field is a `ModelChoiceField`, rendered as a `<select>` with one `<option>` per related row. To build it, Django loads every row of the related table and calls `__str__()` on each. That is fine for a `Category` table with 30 rows and a disaster for `instructor = ForeignKey(User)` when the user table has hundreds of thousands of rows: - one large query per widget, plus whatever each `__str__()` touches; - a page several megabytes big, slow to render in the browser; - multiplied by every inline row that repeats the widget, so a lesson inline with an `editor` foreign key and 30 lessons renders 30 of them. `ManyToManyField` has the same problem with a multi-select. ## autocomplete_fields `autocomplete_fields = ["instructor"]` renders a Select2 input. Nothing is loaded up front beyond the current value; as the editor types, the widget calls the admin's autocomplete JSON view, which returns a page of `{id, text}` results. That view is driven by the **related model's** `ModelAdmin`: 1. The related model must be registered on the same admin site, otherwise the system check `admin.E039` fails. 2. That `ModelAdmin` must define `search_fields`, otherwise `admin.E040` fails; the typed term is matched with its `get_search_results()`. 3. It starts from that admin's `get_queryset(request)`, so row scoping there also limits what can be picked. 4. It applies the field's `limit_choices_to`. 5. Ordering and pagination come from the related admin's `get_ordering()` and `get_paginator()`. 6. The user must have **view or change permission** on the related model, or the view returns permission denied, which prevents the widget from leaking data. The docs add a performance caveat: ordering a huge queryset and searching unindexed fields can still be slow, so index the `search_fields` columns of the related model. ## raw_id_fields `raw_id_fields = ["instructor"]` renders a text input for the primary key, or a comma-separated list of ids for a many-to-many field, plus a magnifying-glass button that opens the related model's change list in a popup to search and pick a row. Next to the input the admin shows the current object's label. ## Choosing between them | | `autocomplete_fields` | `raw_id_fields` | |---|---|---| | Up-front load | current value only | current value only | | Editor experience | type a name, pick from matches | type an id or use the popup | | Requirements | related model registered with `search_fields` | related model's change list for the popup | | Access control | view or change permission on the related model | the popup is an ordinary admin change list | | Works for | `ForeignKey`, `ManyToManyField` | `ForeignKey`, `ManyToManyField` | | Also in inlines | yes | yes | Autocomplete is the usual choice for editor-facing forms. Raw id fits internal tools where operators already work with ids, or when the related model cannot sensibly be searched. ## Customising the results Because the autocomplete view delegates to the related `ModelAdmin`, customisation happens there: - Override the user admin's `get_search_results()` to search the way editors think, for example an exact match on an e-mail address before falling back to name fragments; the docs suggest this for very large tables. - Give the related admin an `ordering` on an indexed column, or none at all, since sorting a huge candidate set is expensive. - Use `limit_choices_to` on the `ForeignKey` (for example only users in the Instructors group) so the choices are narrowed wherever the field appears, including the autocomplete. The label shown for each result is the object's `__str__()`, so make it distinguish people who share a name, for instance by including the e-mail. ## Related options - `formfield_for_foreignkey()` can narrow the default select's queryset instead, when a small subset is all that is valid (only users in the Instructors group). - `filter_horizontal`/`filter_vertical` improve many-to-many selects but still load every row.
- A Django admin autocomplete for instructor returns nothing for some staff users; why might that be?The autocomplete view checks the staff user's view or change permission on the related model and returns permission denied without it. It also starts from the related `ModelAdmin.get_queryset(request)`, so row scoping there hides users, and it applies the field's `limit_choices_to`. Check all three before suspecting the search.
- What system check fails if a Django ModelAdmin puts instructor in autocomplete_fields but the user admin lacks search_fields?`admin.E040`: the related model's `ModelAdmin` must define `search_fields` because the autocomplete search uses it. If the related model is not registered on the admin site at all, `admin.E039` fails instead. Both are raised at startup by the system checks, not when the page is opened.
The default select is a printed phone book stapled to the form: complete, heavy, and reprinted for every page view. Autocomplete is a directory-enquiries call that returns a few names matching what you say; raw id is being handed a customer number and a link to look it up.
saying these in an interview costs you the question
- autocomplete_fields works without the related model being registered in the admin
- raw_id_fields still loads every related row to validate the id
- The autocomplete search uses the current ModelAdmin's search_fields
- Autocomplete results ignore permissions, so any staff user sees every row
- filter_horizontal avoids loading all related rows for many-to-many fields