Django admin: частые ошибки, которые тормозят и ломают админку
Список пользователей грузится десять секунд, права редактируют не тех записей, а поиск валит базу. Разбираем типичные ошибки в Django admin и как их чинить.

Админка Django выглядит бесплатной: зарегистрировал модель, и всё работает. Проблемы начинаются на реальных данных. Список из ста тысяч заказов открывается пятнадцать секунд, потому что на каждую строку идёт отдельный запрос к базе. Менеджер по ошибке видит чужие данные, потому что права раздали на уровне модели, а не объекта. Поиск по тексту кладёт продакшн, потому что search_fields заставляет базу сканировать таблицу целиком. Ниже — самые частые ошибки в django.contrib.admin и что с ними делать. Актуально для Django 4.2 LTS и 5.x, поведение ForeignKey в списках не менялось много версий.
N+1 запросов в списке — главный источник тормозов
Стандартный сценарий: модель Order с полем customer = ForeignKey, и в list_display вы выводите имя клиента. Django рисует 100 строк на странице и делает 100 отдельных запросов за клиентами — плюс один за самими заказами. Это классический N+1.
Лечится list_select_related для внешних ключей и OneToOne:
# admin.py, Django 4.2 / 5.x
from django.contrib import admin
from .models import Order
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ("id", "customer", "status", "created_at")
# без этой строки — по одному запросу на каждого клиента в списке
list_select_related = ("customer",)
Для связей «многие ко многим» и обратных внешних ключей list_select_related не помогает — там нужен prefetch_related. Его в атрибуте не задать, поэтому переопределяют get_queryset:
class OrderAdmin(admin.ModelAdmin):
list_display = ("id", "customer", "tags_count")
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("tags")
@admin.display(description="Тегов")
def tags_count(self, obj):
# данные уже подгружены prefetch_related, лишнего запроса нет
return obj.tags.count()
Как проверить, что помогло: поставьте django-debug-toolbar и откройте список. Панель SQL покажет число запросов. Было 100+ — стало 2-3.
Метод в list_display, который сам делает запрос
Вы добавили в list_display метод, который внутри обращается к связанной таблице или считает агрегат. На каждой строке — новый запрос, и list_select_related тут ни при чём, потому что запрос порождает ваш собственный код.
Правило простое: если метод вlist_displayтрогает базу, данные для него нужно подготовить вget_querysetчерезannotateилиprefetch_related. Иначе N+1 вернётся, даже если внешние ключи вы уже оптимизировали.
Считать в списке через annotate, а не в методе построчно:
from django.db.models import Count
class OrderAdmin(admin.ModelAdmin):
list_display = ("id", "customer", "items_total")
def get_queryset(self, request):
return super().get_queryset(request).annotate(
_items_total=Count("items")
)
@admin.display(description="Позиций", ordering="_items_total")
def items_total(self, obj):
return obj._items_total
Параметр ordering в @admin.display позволяет ещё и сортировать список по вычисленному полю — без него колонка будет несортируемой.
ForeignKey-выпадашка на десять тысяч записей
Форма редактирования заказа открывается медленно и весит мегабайты HTML, потому что поле customer рендерится как <select> со всеми клиентами в базе. При десяти тысячах записей это гарантированный тормоз, а при ста тысячах — таймаут.
Для таких полей есть raw_id_fields и autocomplete_fields:
raw_id_fields— вместо селекта показывает поле с id и ссылку на поиск. Легковесно, но неудобно: пользователь видит цифру, а не имя.autocomplete_fields— поиск с подгрузкой по мере ввода (AJAX). Удобнее всего, но требует, чтобы у связанной модели в её админке был заданsearch_fields, иначе Django выбросит ошибку при загрузке.
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
autocomplete_fields = ("customer",)
# без этого autocomplete для Order.customer не заработает
@admin.register(Customer)
class CustomerAdmin(admin.ModelAdmin):
search_fields = ("name", "email")
search_fields, который сканирует всю таблицу
Поиск в админке по умолчанию строит запрос с LIKE '%текст%'. Ведущий процент отключает использование индекса — база вынуждена читать таблицу целиком. На маленьких данных незаметно, на миллионе строк — секунды блокировки.
Что с этим делать:
- Префикс
^вsearch_fields(search_fields = ("^name",)) даётLIKE 'текст%'— такой запрос уже может использовать обычный B-tree индекс. - Точное совпадение — префикс
=(search_fields = ("=email",)), этоWHERE email = 'текст'по индексу. - Для полнотекстового поиска на PostgreSQL стандартный
search_fieldsне подходит — нужен отдельный поиск черезSearchVector, но это уже за пределами базовой админки.
Проверьте на боевом объёме данных, а не на десятке тестовых записей: именно здесь ошибка проявляется только под нагрузкой.
Права на модель вместо прав на объект
Частая путаница: считают, что стандартные права Django (view, change, delete) умеют ограничивать доступ к конкретным записям. Не умеют. Право change_order означает «может менять любые заказы», а не «свои».
Если менеджер должен видеть только свои заказы, фильтруйте get_queryset по пользователю:
class OrderAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(manager=request.user)
Object-level permissions (права на отдельный объект) в стандартной админке не реализованы полностью — для сложных сценариев обычно берут django-guardian или собственную логику в has_change_permission(self, request, obj). Это отдельная большая тема, здесь ограничиваемся фильтрацией queryset.
Тяжёлая работа в save_model блокирует запрос
В save_model или через сигнал post_save вешают отправку письма, генерацию PDF или вызов внешнего API. Пользователь жмёт «Сохранить» и ждёт, пока отработает сторонний сервис. Если тот отвечает медленно или падает — падает и сохранение в админке.
Тяжёлое выносите в фоновую задачу (Celery, django-q, RQ), а в save_model оставляйте только постановку задачи в очередь. Сохранение объекта не должно зависеть от доступности почтового сервера.
Сводка ошибок и решений
| Ошибка | Симптом | Что применить |
|---|---|---|
| N+1 по ForeignKey в списке | Список грузится секунды | list_select_related |
| N+1 по M2M / обратным связям | То же, но select_related не помог | prefetch_related в get_queryset |
| Метод в list_display с запросом | Много запросов даже после оптимизации FK | annotate + @admin.display(ordering=...) |
| Огромный select в форме | Форма редактирования тормозит | autocomplete_fields или raw_id_fields |
| Поиск сканирует таблицу | Пауза базы при вводе в поиск | префиксы ^ и = в search_fields |
| Доступ к чужим записям | Пользователь видит лишнее | фильтр в get_queryset |
| Медленное сохранение | «Сохранить» висит или падает | фон через Celery/django-q |
Что делать дальше
Поставьте django-debug-toolbar на стейджинге и пройдите по всем разделам админки с реальным объёмом данных — панель SQL покажет проблемные списки быстрее любого чек-листа. Смотрите на два показателя: число запросов на страницу списка (должно быть единицы, а не десятки) и время самого медленного запроса. Инвалидация и кеширование самой админки — отдельная тема, здесь мы разбирали только запросы и права.
Защитник прав потребителей: Досудебные претензии на возврат товара со ссылками на ЗоЗПП.
Спросить за 15 ₽Источники: Django documentation — The Django admin site (ModelAdmin options), django-debug-toolbar documentation
Частые вопросы
У меня Django 4.2, а примеры вроде для 5.x — сработают ли?
Добавил list_select_related, но запросов всё равно много — почему?
Autocomplete выдаёт ошибку при загрузке страницы — что не так?
Стандартные права Django могут ограничить доступ к конкретным записям?
Стоит ли выносить всю логику из админки в отдельный сервис?
Поиск с префиксом ^ точно будет использовать индекс?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.