Слишком много SQL-запросов при использовании инлайн-представления "многие ко многим" в Django Admin
У меня есть "бриллиантовые" отношения между четырьмя моделями. AlignedScan - это, по сути, модель, несущая полезную нагрузку, облегчающая отношения "многие-ко-многим" между Scan и Alignment. И Scan, и Alignment имеют отношения "один к одному" к Scan Project. При попытке отобразить инлайн-список AlignedScan в окне Alignment detail (в Django Admin), это работает, но очень медленно из-за двух дополнительных SQL-запросов, выполняемых для каждой записи. Что я делаю не так?
Это мои модели:
from django.contrib.gis.db import models
from django.db.models import Count, Q
class ScanProject_Manager(models.Manager):
def get_queryset(self):
return (super().get_queryset()
.annotate(number_of_alignments=Count('alignments', distinct=True))
.annotate(number_of_usedscans=Count('scans', distinct=True, filter=Q(scans__used=True)))
)
class ScanProject(models.Model):
objects = ScanProject_Manager()
slug = models.CharField(max_length=30,primary_key=True,)
label = models.CharField(max_length=100,)
#...
class Scan(models.Model):
name = models.CharField(max_length=24,)
#...
scanproject = models.ForeignKey(
ScanProject,
related_name='scans',
on_delete=models.CASCADE,
)
class Alignment(models.Model):
aligned_when = models.CharField(max_length=13,)
#...
scanproject = models.ForeignKey(
ScanProject,
related_name='alignments',
on_delete=models.CASCADE,
)
class AlignedScan(models.Model):
registered = models.BooleanField()
x = models.FloatField(blank=True, null=True,)
y = models.FloatField(blank=True, null=True,)
z = models.FloatField(blank=True, null=True,)
#...
alignment = models.ForeignKey(
Alignment,
related_name='alignedscans',
on_delete=models.CASCADE, \
)
scan = models.ForeignKey(
Scan,
related_name='alignedscans',
on_delete=models.CASCADE,
)
class AlignedScan_Manager(models.Manager): def get_queryset(self): return super().get_queryset().select_related('scan')
Я пробовал с AlignedScan_Manager и без него, я пробовал использовать простую ванильную InlineModel из коробки:
class AlignedScan_Inline(admin.TabularInline):
model = AlignedScan
и я попробовал пользовательский:
class AlignedScan_Inline(admin.TabularInline):
verbose_name = 'Scan' # omit 'Aligned' from the UI, as it is implied
verbose_name_plural = 'Scans'
model = AlignedScan
fk_name = 'alignment'
fields = ('registered', 'x', 'y', 'z')
readonly_fields = ('registered', 'x', 'y', 'z')
extra = 0
show_change_link = True
list_select_related = ['scan'] # tried with and without this
def has_add_permission(self, request, owner):
return False
def has_change_permission(self, request, owner):
return True #TBC: respect user permissions
def has_delete_permission(self, request, owner):
return False #TBC: respect user permissions
Окончательная версия должна также показывать поля из таблицы Scan, но даже когда я этого не делаю, проблема сохраняется.
Я получаю два SQL-запроса для каждой записи:
SELECT "scans_scan"."id",
"scans_scan"."name",
-- ....
"scans_scan"."scanproject_id"
FROM "scans_scan"
WHERE "scans_scan"."id" = 3413
LIMIT 21 10 similar queries.
и
SELECT "scans_alignment"."id",
"scans_alignment"."aligned_when",
"scans_alignment"."scanproject_id",
-- ...
"scans_alignment"."imported_by_id"
FROM "scans_alignment"
WHERE "scans_alignment"."id" = 4
LIMIT 21 11 similar queries. Duplicated 11 times.
Попробуйте добавить поле scan
в raw_id_fields
.
class AlignedScan_Inline(admin.TabularInline):
raw_id_fields = ['scan']
...
Это полезно, потому что ModelForms делает некоторые неэффективные вещи из коробки в отношении полей отношений. Это, очевидно, усугубляется наличием большого количества инлайнов в админке. Удалив поле отношений из процесса рендеринга, вы устраните эту неэффективность.
Попробуйте выбрать связанные поля из get_queryset
:
class AlignedScan_Inline(admin.TabularInline):
...
def get_queryset(self, *args, **kwargs):
return super().get_queryset(*args, **kwargs).select_related('scan', 'alignment')
Дополнительный ответ:
Это также устранило проблему:
class AlignedScan_Manager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('scan', 'alignment')
В моей первой попытке я предварительно выбрал только 'scan', а не 'scan' и 'alignment'. Если я использую этот менеджер для своей модели AlignedScan, я могу удалить переопределение get_queryset() из AlignedScan_Inline