Яка різниця між select_related та prefetch_related у Django ORM?


291

У Django doc,

select_related() "слід" за зв'язками із зовнішніми ключами, вибираючи додаткові дані пов'язаного об'єкта під час виконання запиту.

prefetch_related() робить окремий пошук для кожного відносини і робить "приєднання" в Python.

Що це означає "робити об'єднання в пітон"? Може хтось проілюструє прикладом?

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


2
Виконання з'єднання в python означає, що з'єднання не відбудеться в базі даних. За допомогою функції select_related ваше приєднання відбувається в базі даних, і вам належить лише один запит до бази даних. За допомогою prefetch_related ви будете виконувати два запити, а потім ORM 'з'єднає результати, щоб ви все ще могли ввести object.related_set
Mark Galloway

3
Як виноска, Timmy O'Mahony також може пояснити свої відмінності, використовуючи звернення до бази даних: посилання
Mærcos

Відповіді:


423

Ваше розуміння здебільшого правильне. Ви використовуєте, select_relatedколи об’єкт, який ви збираєтеся вибрати, є одним об'єктом, так OneToOneFieldчи a ForeignKey. Ви використовуєте, prefetch_relatedколи збираєтеся отримати "набір" речей, так ManyToManyFieldщо, як ви заявили, або зворотний ForeignKeys. Просто для уточнення, що я маю на увазі під "зворотним ForeignKeys" ось ось приклад:

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потрібно створити та надіслати список ідентифікаторів для вибору назад на сервер, це може зайняти деякий час. Я не впевнений, чи є приємний спосіб зробити це в транзакції, але я розумію, що Джанго завжди просто надсилає список і каже SELECT ... ДЕ pk IN (..., ..., ...) в основному. У цьому випадку, якщо попередньо встановлені дані є рідкісними (скажімо, об’єкти штату США, пов’язані з адресами людей), це може бути дуже добре, однак якщо це ближче до одного-до-одного, це може втратити багато комунікацій. Якщо ви сумніваєтесь, спробуйте обидва і подивіться, що працює краще.

Все, що обговорювалося вище, в основному стосується зв'язку з базою даних. З боку Python, однак, prefetch_relatedє додаткова перевага, що один об'єкт використовується для представлення кожного об'єкта в базі даних. З select_relatedдублікатами об’єктів буде створено в Python для кожного "батьківського" об'єкта. Оскільки об’єкти в Python мають гідний об'єм пам'яті, це також може врахувати.


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майже напевно буде краще. Якщо у вас є моделі 100s або 1000s, які використовують той же зовнішній ключ, prefetch_relatedце насправді може бути кращим. Між ними вам доведеться протестувати і подивитися, що станеться.
CrazyCasta

1
Я б заперечував ваш коментар щодо попереднього вибору, "загалом не має великого сенсу". Це справедливо для полів FK, позначених унікальними, але в будь-якому місці, де декілька рядків мають однакове значення FK (автор, користувач, категорія, місто тощо), попередній вибір скорочує пропускну здатність між Django та БД, але не дублює рядки. Він також, як правило, використовує менше пам'яті в БД. Будь-яке з них часто важливіше, ніж накладні відомості про один додатковий запит. З огляду на це найкраща відповідь на досить популярне питання, я думаю, що це слід зазначити у відповіді.
Гордон Вріглі

1
@GordonWrigley Так, минув час, коли я це написав, тому я пішов назад і трохи уточнив. Я не впевнений, що згоден з бітом "використовує менше пам'яті в БД", але так, на все. І він може точно використовувати менше пам'яті на стороні Python.
CrazyCasta

26

Обидва методи досягають однієї мети, щоб відмовитися від зайвих запитів db. Але вони використовують різні підходи для ефективності.

Єдиною причиною використання будь-якого з цих методів є те, коли один великий запит кращий для багатьох малих запитів. Django використовує великий запит для створення моделей в пам'яті, а не для виконання запитів на вимогу до бази даних.

select_relatedвиконує з'єднання з кожним пошуком, але розширює вибір, щоб включити стовпці всіх таблиць, що об'єдналися. Однак цей підхід має застереження.

Приєднання може потенційно множити кількість рядків у запиті. Коли ви здійснюєте з'єднання над іноземним ключем або полем один на один, кількість рядків не збільшуватиметься. Однак приєднання багатьох до багатьох не мають цієї гарантії. Отже, Джанго обмежується 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);

Замість того, щоб виконати єдине з'єднання з потенційно занадто великою кількістю рядків, кожна таблиця розділяється на окремий запит.


1

Як говориться в документації Джанго:

prefetch_related ()

Повертає QuerySet, який автоматично отримує в одній партії пов'язані об'єкти для кожного із зазначених пошукових запитів.

Це має схоже призначення на select_related, оскільки обидва розроблені для припинення потоку запитів баз даних, викликаних доступом до пов'язаних об'єктів, але стратегія зовсім інша.

select_related працює, створюючи SQL приєднання та включаючи поля відповідного об'єкта в оператор SELECT. З цієї причини 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


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, відношення з M2- це ForeignKey, воно просто повертає лише 1 запис для будь-якого M2об'єкта. Те ж саме стосується OneToOneFieldі.

Але M3відношення з M2- це ManyToManyFieldяке може повернути будь-яку кількість M1об'єктів.

Розглянемо випадок, коли у вас є 2 M2об’єкти m21, у m22яких однакові 5 асоційованих M3об’єктів з ідентифікаторами 1,2,3,4,5. Коли ви отримуєте пов’язані M3об'єкти для кожного з цих M2об'єктів, якщо ви використовуєте вибрані пов'язані, це буде працювати.

Кроки:

  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об'єкти лише один раз, що покращує продуктивність.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.