Jaka jest różnica między select_related i prefetch_related w Django ORM?

291

W dokumencie Django

select_related() „śledzi” relacje z kluczem obcym, wybierając dodatkowe dane obiektu pokrewnego podczas wykonywania zapytania.

prefetch_related() wykonuje osobne wyszukiwanie dla każdej relacji i wykonuje „łączenie” w Pythonie.

Co to znaczy „robić łączenie w pythonie”? Czy ktoś może zilustrować przykładem?

Rozumiem, że w przypadku relacji klucza obcego użyj select_related; i dla relacji M2M użyj prefetch_related. Czy to jest poprawne?

NeoWang
źródło
2
Wykonanie łączenia w pythonie oznacza, że ​​połączenie nie nastąpi w bazie danych. W przypadku parametru select_related łączenie odbywa się w bazie danych i występuje tylko jedno zapytanie do bazy danych. W przypadku prefetch_related wykonasz dwa zapytania, a następnie wyniki zostaną „połączone” przez ORM, dzięki czemu nadal możesz wpisać object.related_set
Mark Galloway,
3
W przypisie Timmy O'Mahony może również wyjaśnić swoje różnice za pomocą trafień w bazie danych: link
Mærcos
To może ci pomóc learnbatta.com/blog/working-with-select_related-in-django-89
anjaneyulubatta505

Odpowiedzi:

423

Twoje rozumienie jest w większości poprawne. Używasz, select_relatedgdy obiekt, który chcesz wybrać, jest pojedynczym obiektem, tak OneToOneFieldlub a ForeignKey. Używasz, prefetch_relatedgdy masz „zbiór” rzeczy, tak ManyToManyFieldjak powiedziałeś lub odwróciłeś ForeignKey. Aby wyjaśnić, co rozumiem przez „rewersję ForeignKey”, oto przykład:

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

Różnica polega na tym, select_relatedże łączy SQL i dlatego zwraca wyniki jako część tabeli z serwera SQL. prefetch_relatedz drugiej strony wykonuje inne zapytanie i dlatego redukuje zbędne kolumny w oryginalnym obiekcie ( ModelAw powyższym przykładzie). Możesz użyć prefetch_relateddo wszystkiego, czego możesz użyć select_related.

Do kompromisów należy prefetch_relatedutworzenie i wysłanie listy identyfikatorów, aby wybrać je z powrotem na serwer, może to chwilę potrwać. Nie jestem pewien, czy istnieje dobry sposób na zrobienie tego w transakcji, ale rozumiem, że Django zawsze po prostu wysyła listę i mówi WYBIERZ ... GDZIE pk IN (..., ..., ...) gruntownie. W takim przypadku, jeśli wstępnie pobrane dane są rzadkie (powiedzmy obiekty stanu USA powiązane z adresami osób), może to być bardzo dobre, jednak jeśli jest bliżej jeden do jednego, może to zmarnować dużo komunikacji. W razie wątpliwości wypróbuj jedno i drugie i sprawdź, który z nich działa lepiej.

Wszystko omówione powyżej dotyczy w zasadzie komunikacji z bazą danych. Po stronie Pythona prefetch_relatedma jednak tę dodatkową zaletę, że do reprezentowania każdego obiektu w bazie danych służy jeden obiekt. Ze select_relatedzduplikowanymi obiektami zostaną utworzone w Pythonie dla każdego „rodzica” obiektu. Ponieważ obiekty w Pythonie mają sporo miejsca w pamięci, może to również być brane pod uwagę.

CrazyCasta
źródło
3
co jest jednak szybsze?
elad srebrny
24
select_relatedto jedno zapytanie, podczas gdy prefetch_relateddwa, więc pierwsze jest szybsze. Ale select_relatednie pomoże w ManyToManyField„s
bhinesley
31
@eladsilver Przepraszamy za powolną odpowiedź. To zależy. select_relatedużywa JOIN w SQL, podczas gdy prefetch_relateduruchom zapytanie w pierwszym modelu, zbiera wszystkie identyfikatory potrzebne do pobrania z wyprzedzeniem, a następnie uruchamia zapytanie z klauzulą ​​IN w GDZIE ze wszystkimi potrzebnymi identyfikatorami. Jeśli powiesz, że 3-5 modeli używa tego samego klucza obcego, select_relatedprawie na pewno będzie lepiej. Jeśli masz setki lub tysiące modeli używających tego samego klucza obcego, prefetch_relatedmoże być lepiej. W międzyczasie będziesz musiał przetestować i zobaczyć, co się stanie.
CrazyCasta
1
Nie zgadzam się z twoim komentarzem na temat pobierania wstępnego „ogólnie nie ma większego sensu”. Dotyczy to pól FK oznaczonych jako unikalne, ale wszędzie tam, gdzie wiele wierszy ma tę samą wartość FK (autor, użytkownik, kategoria, miasto itp.), Pobieranie wstępne zmniejsza przepustowość między Django i DB, ale nie powiela wierszy. Zasadniczo zużywa również mniej pamięci na DB. Każda z nich jest często ważniejsza niż narzut związany z pojedynczym dodatkowym zapytaniem. Biorąc pod uwagę, że jest to najlepsza odpowiedź na dość popularne pytanie, myślę, że należy to odnotować w odpowiedzi.
Gordon Wrigley,
1
@GordonWrigley Tak, minęło trochę czasu, odkąd to napisałem, więc wróciłem i trochę wyjaśniłem. Nie jestem pewien, czy zgadzam się z bitem „zużywa mniej pamięci w DB”, ale tak we wszystkim. I z pewnością może zużywać mniej pamięci po stronie Pythona.
CrazyCasta
26

Obie metody osiągają ten sam cel, aby zrezygnować z niepotrzebnych zapytań db. Ale używają różnych podejść do wydajności.

Jedynym powodem użycia jednej z tych metod jest to, że jedno duże zapytanie jest lepsze niż wiele małych zapytań. Django używa dużego zapytania, aby zapobiegawczo tworzyć modele w pamięci zamiast wykonywać zapytania na żądanie względem bazy danych.

select_relatedwykonuje łączenie przy każdym wyszukiwaniu, ale rozszerza zaznaczenie, aby uwzględnić kolumny wszystkich połączonych tabel. Jednak takie podejście ma pewne zastrzeżenie.

Połączenia mogą potencjalnie pomnożyć liczbę wierszy w zapytaniu. Kiedy wykonujesz sprzężenie na kluczu obcym lub polu jeden do jednego, liczba wierszy nie wzrośnie. Jednak połączenia wielu do wielu nie mają tej gwarancji. Django ograniczaselect_related do relacji, które nieoczekiwanie nie spowodują masowego łączenia.

The „Przyłączyć pytona” dla prefetch_relatedtrochę bardziej niepokojące to powinno być. Tworzy osobne zapytanie dla każdej tabeli, która ma zostać połączona. Filtruje każdą z tych tabel za pomocą klauzuli WHERE IN, na przykład:

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

Zamiast wykonywania pojedynczego łączenia z potencjalnie zbyt wieloma wierszami, każda tabela jest podzielona na osobne zapytanie.

cdosborn
źródło
1

Jak mówi dokumentacja Django:

prefetch_related ()

Zwraca zestaw QuerySet, który automatycznie pobierze, w pojedynczej partii, powiązane obiekty dla każdego z określonych wyszukiwań.

Ma to podobny cel jak select_related, ponieważ oba mają na celu powstrzymanie zalewu zapytań do bazy danych spowodowanego dostępem do powiązanych obiektów, ale strategia jest zupełnie inna.

select_related działa, tworząc sprzężenie SQL i włączając pola powiązanego obiektu w instrukcji SELECT. Z tego powodu select_related pobiera powiązane obiekty w tym samym zapytaniu do bazy danych. Jednak, aby uniknąć znacznie większego zestawu wyników, który powstałby w wyniku połączenia w relacji „wiele”, select_related ogranicza się do relacji o pojedynczej wartości - klucza obcego i jeden do jednego.

Z drugiej strony prefetch_related wykonuje osobne wyszukiwanie dla każdej relacji i wykonuje „łączenie” w Pythonie. Pozwala to na wstępne pobieranie obiektów wiele do wielu i wiele do jednego, czego nie można zrobić przy użyciu select_related, oprócz klucza obcego i relacji jeden do jednego, które są obsługiwane przez select_related. Obsługuje również wstępne pobieranie GenericRelation i GenericForeignKey, jednak musi być ograniczone do jednorodnego zestawu wyników. Na przykład pobieranie obiektów wskazanych przez GenericForeignKey jest obsługiwane tylko wtedy, gdy zapytanie jest ograniczone do jednego ContentType.

Więcej informacji na ten temat: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related

Amin.B
źródło
1

Przejrzał już opublikowane odpowiedzi. Pomyślałem, że byłoby lepiej, jeśli dodam odpowiedź z faktycznym przykładem.

Powiedzmy, że masz 3 powiązane modele 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)

Tutaj możesz zapytać o M2model i jego względne M1obiekty za pomocą select_relationpola i M3obiektów za pomocą prefetch_relationpola.

Jednak, jak wspomnieliśmy M1, relacja z M2jest ForeignKey, to zwraca tylko 1 rekord dla dowolnego M2obiektu. To samo dotyczy OneToOneFieldrównież.

Ale M3relacja z M2jest tą, ManyToManyFieldktóra może zwrócić dowolną liczbę M1obiektów.

Rozważ przypadek, w którym masz 2 M2przedmioty m21, m22które mają takie same 5M3 obiektów powiązanych z identyfikatorami 1,2,3,4,5. Podczas pobierania powiązanych M3obiektów dla każdego z nichM2 obiektów, jeśli użyjesz opcji „Wybierz powiązane”, tak to będzie działać.

Kroki:

  1. Odnaleźć m21 obiekt.
  2. Zapytaj wszystkie M3obiekty związane zm21 obiektami, których identyfikatory to1,2,3,4,5 .
  3. Powtórz to samo dla m22obiektu i wszystkich innychM2 obiektów.

Ponieważ mamy te same 1,2,3,4,5identyfikatory dla obum21 ,m22 obiektów, jeśli używamy opcji select_related, to będzie kwerendy DB dwukrotnie za te same identyfikatory, które były już naciągane.

Zamiast tego, jeśli użyjesz parametru prefetch_related, przy próbie uzyskania M2obiektów zanotuje wszystkie identyfikatory zwrócone przez Twoje obiekty (Uwaga: tylko identyfikatory) podczas zapytania do M2tabeli, a na ostatnim etapie Django wykona zapytanie do M3tabeli z zestawem wszystkich twoich identyfikatorówM2 zwróconych przez obiekty. i dołącz do nichM2 obiektów za pomocą Pythona zamiast bazy danych.

W ten sposób odpytujesz wszystkie M3obiekty tylko raz, co poprawia wydajność.

Jarvis
źródło