В чем разница между select_related и prefetch_related в Django ORM?

291

В Django Doc,

select_related() "следует" отношениям внешнего ключа, выбирая дополнительные данные связанного объекта, когда он выполняет свой запрос.

prefetch_related() выполняет отдельный поиск для каждого отношения и выполняет «соединение» в Python.

Что это значит под "объединением в python"? Может кто-нибудь проиллюстрировать примером?

Насколько я понимаю, для отношений с внешним ключом используйте select_related; и для отношений M2M используйте prefetch_related. Это верно?

NeoWang
источник
2
Выполнение объединения в python означает, что соединение не произойдет в базе данных. С select_related ваше соединение происходит в базе данных, и вы переносите только один запрос к базе данных. С prefetch_related вы будете выполнять два запроса, а затем результаты будут «объединены» с помощью ORM, так что вы все равно сможете набирать object.related_set
Марк Галлоуэй,
3
В качестве сноски Тимми О'Махони может также объяснить свои различия с помощью обращений к базе данных: ссылка
Mærcos
Это может помочь вам learnbatta.com/blog/working-with-select_related-in-django-89
anjaneyulubatta505

Ответы:

424

Ваше понимание в основном верно. Вы используете, select_relatedкогда объект, который вы собираетесь выбрать, представляет собой один объект, так что OneToOneFieldили ForeignKey. Вы используете, prefetch_relatedкогда собираетесь получить «набор» вещей, так же, ManyToManyFieldкак вы заявили, или наоборот ForeignKey. Просто чтобы прояснить, что я имею в виду под «обратным ForeignKeyс», вот пример:

class ModelA(models.Model):
    pass

class ModelB(models.Model):
    a = ForeignKey(ModelA)

ModelB.objects.select_related('a').all() # Forward ForeignKey relationship
ModelA.objects.prefetch_related('modelb_set').all() # Reverse ForeignKey relationship

Разница в том, что select_relatedвыполняется соединение SQL и, следовательно, результаты возвращаются как часть таблицы с сервера SQL. prefetch_relatedс другой стороны, выполняет другой запрос и, следовательно, уменьшает избыточные столбцы в исходном объекте ( ModelAв приведенном выше примере). Вы можете использовать prefetch_relatedдля всего, что вы можете использовать select_relatedдля.

Компромисс prefetch_relatedзаключается в том, что необходимо создать и отправить список идентификаторов для выбора обратно на сервер, это может занять некоторое время. Я не уверен, есть ли хороший способ сделать это в транзакции, но я понимаю, что Django всегда просто отправляет список и говорит SELECT ... WHERE pk IN (..., ..., ...) в принципе. В этом случае, если предварительно выбранные данные редки (скажем, объекты Государства США, связанные с адресами людей), это может быть очень хорошо, однако, если они ближе к однозначному, это может привести к потере большого количества сообщений. Если есть сомнения, попробуйте оба варианта и посмотрите, какие из них работают лучше.

Все, что обсуждалось выше, в основном касается связи с базой данных. Однако на стороне Python prefetch_relatedесть дополнительное преимущество в том, что один объект используется для представления каждого объекта в базе данных. При этом select_relatedдубликаты объектов будут создаваться в Python для каждого «родительского» объекта. Так как объекты в Python имеют приличную долю памяти, это также может быть рассмотрено.

CrazyCasta
источник
3
что быстрее, хотя?
Послание серебро
24
select_relatedэто один запрос, а prefetch_relatedдва, так что первый быстрее. Но select_relatedне поможет для ManyToManyField«s
bhinesley
31
@eladsilver Извините за медленный ответ. Это на самом деле зависит. select_relatedиспользует JOIN в SQL, тогда как prefetch_relatedзапускает запрос для первой модели, собирает все идентификаторы, необходимые для предварительной выборки, а затем запускает запрос с предложением IN в WHERE со всеми необходимыми идентификаторами. Если вы скажете 3-5 моделей, использующих один и тот же внешний ключ, select_relatedпочти наверняка будет лучше. Если у вас есть сотни или тысячи моделей, использующих один и тот же внешний ключ, prefetch_relatedможет быть лучше. В промежутке между тем вам придется проверить и посмотреть, что произойдет.
CrazyCasta
1
Я бы оспорил ваш комментарий по поводу предварительной выборки, "как правило, не имеет особого смысла". Это верно для полей FK, помеченных как уникальные, но везде, где несколько строк имеют одинаковое значение FK (автор, пользователь, категория, город и т. Д.), Предварительная выборка сокращает пропускную способность между Django и БД, но не дублирует строки. Он также обычно использует меньше памяти на БД. Любой из них часто более важен, чем издержки одного дополнительного запроса. Учитывая, что это лучший ответ на достаточно популярный вопрос, я думаю, что следует отметить в ответе.
Гордон Ригли
1
@GordonWrigley Да, с тех пор, как я написал это, прошло много времени, поэтому я вернулся и немного разъяснил. Я не уверен, что согласен с битом «использует меньше памяти на БД», но да, ко всему. И он может использовать меньше памяти на стороне Python.
CrazyCasta
26

Оба метода достигают одной и той же цели, чтобы избежать ненужных запросов к базе данных. Но они используют разные подходы для эффективности.

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

select_relatedвыполняет соединение с каждым поиском, но расширяет выбор, чтобы включить столбцы всех соединенных таблиц. Однако у этого подхода есть одна оговорка.

Объединения могут умножить количество строк в запросе. Когда вы выполняете объединение по внешнему ключу или однозначному полю, количество строк не увеличивается. Однако объединения «многие ко многим» не имеют такой гарантии. Таким образом, Django ограничивается select_relatedотношениями, которые не могут неожиданно привести к массовому объединению.

«Присоединиться питон» для prefetch_relatedнемного более тревожной , то это должно быть. Он создает отдельный запрос для каждой таблицы, к которой нужно присоединиться. Он фильтрует каждую из этих таблиц с предложением WHERE IN, например:

SELECT "credential"."id",
       "credential"."uuid",
       "credential"."identity_id"
FROM   "credential"
WHERE  "credential"."identity_id" IN
    (84706, 48746, 871441, 84713, 76492, 84621, 51472);

Вместо того, чтобы выполнять одно соединение с потенциально слишком большим количеством строк, каждая таблица разбивается на отдельный запрос.

cdosborn
источник
1

Как сказано в документации Django:

prefetch_related ()

Возвращает QuerySet, который автоматически извлекает в одном пакете связанные объекты для каждого из указанных поисков.

Это имеет аналогичное назначение для select_related в том смысле, что оба предназначены для остановки потока запросов к базе данных, вызванного доступом к связанным объектам, но стратегия совершенно иная.

select_related работает путем создания соединения SQL и включения полей связанного объекта в оператор SELECT. По этой причине select_related получает связанные объекты в одном запросе к базе данных. Тем не менее, чтобы избежать гораздо большего результирующего набора, который мог бы возникнуть в результате объединения по многим отношениям, select_related ограничен однозначными отношениями - внешним ключом и взаимно-однозначным.

prefetch_related, с другой стороны, выполняет отдельный поиск для каждого отношения и выполняет «соединение» в Python. Это позволяет ему предварительно выбирать объекты «многие ко многим» и «многие к одному», что невозможно сделать с помощью select_related, в дополнение к внешнему ключу и отношениям «один к одному», которые поддерживаются select_related. Он также поддерживает предварительную выборку GenericRelation и GenericForeignKey, однако он должен быть ограничен однородным набором результатов. Например, предварительная выборка объектов, на которые ссылается GenericForeignKey, поддерживается только в том случае, если запрос ограничен одним ContentType.

Подробнее об этом: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related

Amin.B
источник
1

Перебрал уже выложенные ответы. Просто подумал, что будет лучше, если я добавлю ответ с реальным примером.

Допустим, у вас есть 3 модели Django, которые связаны между собой.

class M1(models.Model):
    name = models.CharField(max_length=10)

class M2(models.Model):
    name = models.CharField(max_length=10)
    select_relation = models.ForeignKey(M1, on_delete=models.CASCADE)
    prefetch_relation = models.ManyToManyField(to='M3')

class M3(models.Model):
    name = models.CharField(max_length=10)

Здесь вы можете запросить M2модель и ее относительные M1объекты, используя select_relationполе, и M3объекты, используя prefetch_relationполе.

Однако, как мы уже упоминали M1, отношение from M2является a ForeignKey, оно просто возвращает только 1 запись для любого M2объекта. То же самое относится и к OneToOneField.

Но M3соотношение «S от M2это , ManyToManyFieldкоторое может возвращать любое количество M1объектов.

Рассмотрим случай, когда у вас есть 2 M2объекта m21, m22которые имеют одинаковые 5 связанных M3объектов с идентификаторами 1,2,3,4,5. Когда вы выбираете связанные M3объекты для каждого из этих M2объектов, если вы используете select related, это то, как это будет работать.

шаги:

  1. Найти m21объект.
  2. Запросите все M3объекты, связанные с m21объектом, чьи идентификаторы 1,2,3,4,5.
  3. Повторите то же самое для m22объекта и всех других M2объектов.

Так как мы имеем одинаковые 1,2,3,4,5идентификаторы для обоих m21, m22объекты, если мы используем select_related вариант, он будет запрашивать БД дважды для одних и тех же идентификаторов , которые уже были извлечены.

Вместо этого, если вы используете prefetch_related, когда вы пытаетесь получить M2объекты, он запомнит все идентификаторы, которые ваши объекты вернули (Примечание: только идентификаторы) при запросе M2таблицы и в качестве последнего шага, Django собирается сделать запрос к M3таблице с набором всех идентификаторов, которые ваши M2объекты вернули. и присоединить их к M2объектам, используя Python вместо базы данных.

Таким образом, вы запрашиваете все M3объекты только один раз, что повышает производительность.

Jarvis
источник