Qual è la differenza tra select_related e prefetch_related in Django ORM?


291

Nel documento Django,

select_related() "segue" le relazioni di chiave esterna, selezionando ulteriori dati relativi all'oggetto correlato quando esegue la sua query.

prefetch_related() esegue una ricerca separata per ogni relazione e esegue il "join" in Python.

Che cosa significa "fare l'unione in Python"? Qualcuno può illustrare con un esempio?

La mia comprensione è che per la relazione chiave esterna, utilizzare select_related; e per la relazione M2M, utilizzare prefetch_related. È corretto?


2
L'esecuzione del join in Python significa che il join non avverrà nel database. Con select_related, il tuo join si verifica nel database e subisci solo una query del database. Con prefetch_related, eseguirai due query e quindi i risultati verranno "uniti" dall'ORM in modo da poter ancora digitare object.related_set
Mark Galloway,

3
Come nota a piè di pagina, Timmy O'Mahony può anche spiegare le loro differenze usando i risultati del database: link
Mærcos

Risposte:


424

La tua comprensione è per lo più corretta. Si utilizza select_relatedquando l'oggetto che si intende selezionare è un singolo oggetto, quindi OneToOneFieldo a ForeignKey. Usi prefetch_relatedquando hai intenzione di ottenere un "insieme" di cose, così ManyToManyFieldcome hai detto o al contrario ForeignKey. Giusto per chiarire cosa intendo per "reverse ForeignKeys" ecco un esempio:

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

La differenza è che select_relatedfa un join SQL e quindi ottiene i risultati come parte della tabella dal server SQL. prefetch_relatedd'altra parte esegue un'altra query e quindi riduce le colonne ridondanti nell'oggetto originale ( ModelAnell'esempio sopra). È possibile utilizzare prefetch_relatedper tutto ciò per cui è possibile utilizzare select_related.

I compromessi sono che prefetch_relateddeve creare e inviare un elenco di ID da selezionare nuovamente sul server, questo può richiedere del tempo. Non sono sicuro che ci sia un buon modo per farlo in una transazione, ma la mia comprensione è che Django manda sempre un elenco e dice SELEZIONA ... DOVE pk IN (..., ..., ...) fondamentalmente. In questo caso, se i dati precaricati sono scarsi (diciamo oggetti degli Stati Uniti collegati agli indirizzi delle persone) questo può essere molto buono, tuttavia se è più vicino a uno a uno, questo può sprecare molte comunicazioni. In caso di dubbi, provare entrambi e vedere quale funziona meglio.

Tutto quanto discusso sopra riguarda fondamentalmente le comunicazioni con il database. Sul lato Python prefetch_relatedha tuttavia il vantaggio extra che un singolo oggetto viene utilizzato per rappresentare ogni oggetto nel database. Con select_relatedoggetti duplicati verranno creati in Python per ogni oggetto "genitore". Poiché gli oggetti in Python hanno un discreto overhead di memoria, anche questa può essere una considerazione.


3
cosa è più veloce però?
elad silver

24
select_relatedè una query mentre prefetch_relatedè due, quindi la prima è più veloce. Ma select_relatednon ti aiuterà per ManyToManyField's
bhinesley,

31
@eladsilver Ci scusiamo per la risposta lenta. In realtà dipende. select_relatedutilizza un JOIN in SQL mentre prefetch_relatedesegue la query sul primo modello, raccoglie tutti gli ID necessari per precaricare e quindi esegue una query con una clausola IN in WHERE con tutti gli ID necessari. Se hai detto 3-5 modelli che usano la stessa chiave esterna, select_relatedquasi sicuramente sarà migliore. Se hai 100 o 1000 di modelli che usano la stessa chiave esterna, prefetch_relatedpotrebbe effettivamente essere migliore. Nel mezzo dovrai testare e vedere cosa succede.
CrazyCasta,

1
Vorrei contestare il tuo commento sul prefetch relativo "generalmente non ha molto senso". Questo è vero per i campi FK contrassegnati come unici, ma ovunque in cui più righe abbiano lo stesso valore FK (autore, utente, categoria, città ecc.) Il prefetch riduce la larghezza di banda tra Django e il DB ma non duplica le righe. Inoltre, utilizza generalmente meno memoria sul DB. Uno di questi è spesso più importante del sovraccarico di una singola query aggiuntiva. Dato che questa è la risposta migliore a una domanda ragionevolmente popolare, penso che dovrebbe essere notato nella risposta.
Gordon Wrigley,

1
@GordonWrigley Sì, è passato un po 'di tempo da quando l'ho scritto, quindi sono tornato e ho chiarito un po'. Non sono sicuro di essere d'accordo con il bit "utilizza meno memoria sul DB", ma sì a tutto. E può sicuramente usare meno memoria sul lato Python.
CrazyCasta,

26

Entrambi i metodi raggiungono lo stesso scopo, per evitare inutili query db. Ma usano approcci diversi per l'efficienza.

L'unico motivo per utilizzare uno di questi metodi è quando una singola query di grandi dimensioni è preferibile a molte query di piccole dimensioni. Django utilizza la query di grandi dimensioni per creare modelli in memoria preventivamente anziché eseguire query su richiesta sul database.

select_relatedesegue un join con ogni ricerca, ma estende la selezione per includere le colonne di tutte le tabelle unite. Tuttavia, questo approccio ha un avvertimento.

I join possono potenzialmente moltiplicare il numero di righe in una query. Quando si esegue un join su una chiave esterna o su un campo uno a uno, il numero di righe non aumenta. Tuttavia, i join molti-a-molti non hanno questa garanzia. Quindi, Django si limita select_relatedalle relazioni che non si tradurranno inaspettatamente in un'unione massiccia.

Il "join in Python" per prefetch_relatedè un po 'più allarmante di quanto dovrebbe essere. Crea una query separata per ogni tabella da unire. Filtra ciascuna di queste tabelle con una clausola WHERE IN, come:

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

Invece di eseguire un singolo join con potenzialmente troppe righe, ogni tabella viene suddivisa in una query separata.


1

Come dice la documentazione di Django:

prefetch_related ()

Restituisce un QuerySet che recupererà automaticamente, in un singolo batch, oggetti correlati per ciascuna delle ricerche specificate.

Questo ha uno scopo simile a select_related, in quanto entrambi sono progettati per arrestare il diluvio di query del database causato dall'accesso a oggetti correlati, ma la strategia è piuttosto diversa.

select_related funziona creando un join SQL e includendo i campi dell'oggetto correlato nell'istruzione SELECT. Per questo motivo, select_related ottiene gli oggetti correlati nella stessa query del database. Tuttavia, per evitare l'insieme di risultati molto più grande che risulterebbe dall'unire una relazione "molti", select_related è limitato alle relazioni a valore singolo: chiave esterna e one-to-one.

prefetch_related, d'altra parte, esegue una ricerca separata per ogni relazione e esegue il 'join' in Python. Ciò consente di precaricare oggetti molti-a-molti e molti-a-uno, che non possono essere eseguiti utilizzando select_related, oltre alla chiave esterna e alle relazioni one-to-one supportate da select_related. Supporta anche il prefetching di GenericRelation e GenericForeignKey, tuttavia, deve essere limitato a un set omogeneo di risultati. Ad esempio, il prefetch degli oggetti a cui fa riferimento un GenericForeignKey è supportato solo se la query è limitata a un ContentType.

Ulteriori informazioni al riguardo: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related


1

Passato attraverso le risposte già pubblicate. Ho pensato che sarebbe meglio se aggiungessi una risposta con l'esempio reale.

Supponiamo che tu abbia 3 modelli Django correlati.

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)

Qui è possibile eseguire una query sul M2modello e sui relativi M1oggetti utilizzando select_relationcampo e M3oggetti utilizzando prefetch_relationcampo.

Tuttavia, come abbiamo già detto M1, la relazione M2è a ForeignKey, restituisce solo 1 record per qualsiasi M2oggetto. Lo stesso vale anche per OneToOneField.

Ma M3la relazione da M2è una ManyToManyFieldche potrebbe restituire qualsiasi numero di M1oggetti.

Prendi in considerazione un caso in cui hai 2 M2oggetti m21, a m22cui sono associati gli stessi 5M3 oggetti con ID 1,2,3,4,5. Quando si recuperano M3oggetti associati per ciascuno di tali M2oggetti, se si utilizza Seleziona correlati, è così che funzionerà.

passi:

  1. Trova m21oggetto.
  2. Interroga tutti gli M3oggetti correlati m21all'oggetto i cui ID sono 1,2,3,4,5.
  3. Ripeti la stessa cosa per l' m22oggetto e tutti gli altri M2oggetti.

Come abbiamo stessi 1,2,3,4,5gli ID per entrambi m21, m22gli oggetti, se si utilizza l'opzione select_related, sta andando a interrogare il DB due volte per lo stesso ID che erano già inverosimile.

Invece se usi prefetch_related, quando tenti di ottenere M2oggetti, prenderà nota di tutti gli ID restituiti dai tuoi oggetti (Nota: solo gli ID) durante l'interrogazione della M2tabella e come ultimo passaggio, Django eseguirà una query alla M3tabella con l'insieme di tutti gli ID M2restituiti dagli oggetti. e uniscili agli M2oggetti usando Python invece del database.

In questo modo stai interrogando tutti gli M3oggetti solo una volta, migliorando le prestazioni.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.