Szyny: dołączają vs.


345

Jest to raczej pytanie „dlaczego to działa w ten sposób”, a nie pytanie „nie wiem jak to zrobić” ...

Dlatego ewangelią związaną z :includepobieraniem powiązanych rekordów, z których wiesz, że będziesz korzystać, jest skorzystanie, ponieważ dostaniesz połączenie i unikniesz mnóstwa dodatkowych zapytań:

Post.all(:include => :comments)

Jednak gdy spojrzysz na dzienniki, nie nastąpi żadne połączenie:

Post Load (3.7ms)   SELECT * FROM "posts"
Comment Load (0.2ms)   SELECT "comments.*" FROM "comments" 
                       WHERE ("comments".post_id IN (1,2,3,4)) 
                       ORDER BY created_at asc) 

To jest na skróty, ponieważ ciągnie wszystkie komentarze na raz, ale to nie jest jeszcze dołączyć (czyli to, co wydaje się cała dokumentacja powiedzieć). Jedynym sposobem na uzyskanie sprzężenia jest użycie :joinszamiast :include:

Post.all(:joins => :comments)

A dzienniki pokazują:

Post Load (6.0ms)  SELECT "posts".* FROM "posts" 
                   INNER JOIN "comments" ON "posts".id = "comments".post_id

Czy coś brakuje? Mam aplikację z pół tuzinem skojarzeń i na jednym ekranie wyświetlam dane ze wszystkich. Wydaje się, że lepiej byłoby mieć jedno zapytanie łączone zamiast 6 osób. Wiem, że pod względem wydajności nie zawsze lepiej jest łączyć zamiast pojedynczych zapytań (w rzeczywistości jeśli spędzasz czas, wygląda na to, że dwa powyższe zapytania są szybsze niż łączenie), ale po wszystkich dokumentach Czytam, jestem zaskoczony, widząc, że :includenie działa zgodnie z reklamą.

Może Railsy świadome problemu z wydajnością i nie dołączają, z wyjątkiem niektórych przypadków?


3
jeśli korzystasz ze starszej wersji Railsów, zaznacz to za pomocą tagów lub w treści pytania. W przeciwnym razie, jeśli używasz Rails 4 TERAZ, jest includes(dla każdego, kto to czyta)
onebree

Jest też teraz: preload i: eager_load blog.bigbinary.com/2013/07/01/…
CJW

Odpowiedzi:


179

Wygląda na to, że :includefunkcjonalność została zmieniona w Rails 2.1. Szyny zawsze wykonywały łączenie we wszystkich przypadkach, ale ze względu na wydajność zmieniono tak, aby w niektórych okolicznościach korzystało z wielu zapytań. Ten post na blogu autorstwa Fabio Akita zawiera kilka dobrych informacji na temat zmiany (zobacz sekcję „Zoptymalizowane szybkie ładowanie”).



To bardzo pomocne, dzięki. Chciałbym jednak, aby istniał sposób, aby zmusić Rails do wykonania połączenia, nawet bez wymaganego „gdzie”. W niektórych przypadkach wiesz, że łączenie będzie bardziej wydajne i nie będzie wiązało się z ryzykiem duplikacji.
Jonathan Swartz


@JonathanSwartz Wygląda na to, że nowa wersja Railsy obsługują to za pomocą eagerload . Dzięki za link NathanLong
rubyprince

92

.joinspo prostu dołącza do tabel i zwraca wybrane pola w zamian. jeśli wywołasz asocjacje przy wyniku zapytania o przyłączenie, ponownie uruchomi zapytania do bazy danych

:includeschętnie załaduje dołączone skojarzenia i doda je do pamięci. :includesładuje wszystkie zawarte atrybuty tabel. Jeśli wywołasz skojarzenia przy włączonym wyniku zapytania, nie będzie on uruchamiał żadnych zapytań


71

Różnica między łączeniami i włączaniem polega na tym, że użycie instrukcji włącza generuje znacznie większe zapytanie SQL ładujące do pamięci wszystkie atrybuty z innych tabel.

Na przykład, jeśli masz tabelę pełną komentarzy i używasz: joins => users, aby pobrać wszystkie informacje o użytkowniku do celów sortowania itp., To będzie działać poprawnie i zajmie mniej czasu niż: include, ale powiedz, że chcesz wyświetlić komentarz wraz z nazwą użytkownika, adresem e-mail itp. Aby uzyskać informacje za pomocą: łączy, będzie musiał utworzyć osobne zapytania SQL dla każdego pobieranego użytkownika, a jeśli użyłeś: dołącz tę informację, jest gotowa do użycia.

Świetny przykład:

http://railscasts.com/episodes/181-include-vs-joins


55

Byłem niedawno czytać więcej na różnicy pomiędzy :joinsi :includesna szynach. Oto wyjaśnienie tego, co zrozumiałem (z przykładami :))

Rozważ ten scenariusz:

  • Użytkownik ma wiele komentarzy, a komentarz należy do użytkownika.

  • Model użytkownika ma następujące atrybuty: Nazwa (ciąg), Wiek (liczba całkowita). Model komentarza ma następujące atrybuty: Content, user_id. W przypadku komentarza identyfikator_użytkownika może mieć wartość NULL.

Dołącza:

: joins wykonuje wewnętrzne połączenie między dwiema tabelami. A zatem

Comment.joins(:user)

#=> <ActiveRecord::Relation [#<Comment id: 1, content: "Hi I am Aaditi.This is my first   comment!", user_id: 1, created_at: "2014-11-12 18:29:24", updated_at: "2014-11-12 18:29:24">, 
     #<Comment id: 2, content: "Hi I am Ankita.This is my first comment!", user_id: 2, created_at: "2014-11-12 18:29:29", updated_at: "2014-11-12 18:29:29">,    
     #<Comment id: 3, content: "Hi I am John.This is my first comment!", user_id: 3, created_at: "2014-11-12 18:30:25", updated_at: "2014-11-12 18:30:25">]>

pobierze wszystkie rekordy, w których user_id (tabeli komentarzy) jest równy user.id (tabeli użytkowników). Zatem jeśli tak

Comment.joins(:user).where("comments.user_id is null")

#=> <ActiveRecord::Relation []>

Otrzymasz pustą tablicę, jak pokazano.

Ponadto sprzężenia nie ładują połączonej tabeli do pamięci. Zatem jeśli tak

comment_1 = Comment.joins(:user).first

comment_1.user.age
#=>←[1m←[36mUser Load (0.0ms)←[0m  ←[1mSELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT 1←[0m  [["id", 1]]
#=> 24

Jak widać, comment_1.user.ageponownie uruchomi zapytanie bazy danych w tle, aby uzyskać wyniki

Obejmuje:

: obejmuje wykonuje lewe połączenie zewnętrzne między dwiema tabelami. A zatem

Comment.includes(:user)

#=><ActiveRecord::Relation [#<Comment id: 1, content: "Hi I am Aaditi.This is my first comment!", user_id: 1, created_at: "2014-11-12 18:29:24", updated_at: "2014-11-12 18:29:24">,
   #<Comment id: 2, content: "Hi I am Ankita.This is my first comment!", user_id: 2, created_at: "2014-11-12 18:29:29", updated_at: "2014-11-12 18:29:29">,
   #<Comment id: 3, content: "Hi I am John.This is my first comment!", user_id: 3, created_at: "2014-11-12 18:30:25", updated_at: "2014-11-12 18:30:25">,    
   #<Comment id: 4, content: "Hi This is an anonymous comment!", user_id: nil, created_at: "2014-11-12 18:31:02", updated_at: "2014-11-12 18:31:02">]>

spowoduje połączenie tabeli ze wszystkimi rekordami z tabeli komentarzy. Zatem jeśli tak

Comment.includes(:user).where("comment.user_id is null")
#=> #<ActiveRecord::Relation [#<Comment id: 4, content: "Hi This is an anonymous comment!", user_id: nil, created_at: "2014-11-12 18:31:02", updated_at: "2014-11-12 18:31:02">]>

pobierze rekordy, w których comments.user_id ma wartość zero, jak pokazano.

Ponadto zawiera obciążenia obu tabel w pamięci. Zatem jeśli tak

comment_1 = Comment.includes(:user).first

comment_1.user.age
#=> 24

Jak widać, komentarz_1.user.age po prostu ładuje wynik z pamięci bez uruchamiania zapytania bazy danych w tle.


Czy to dotyczy Rails 4?
onebree

@HunterStevens: Tak to jest
Aaditi Jain,

54

Oprócz względów wydajnościowych istnieje również funkcjonalna różnica. Kiedy dołączasz do komentarzy, pytasz o posty z komentarzami - domyślnie jest to połączenie wewnętrzne. Gdy dołączysz komentarze, poprosisz o wszystkie posty - dołączenie zewnętrzne.


10

tl; dr

Kontrastuję je na dwa sposoby:

złączenia - do warunkowego wyboru rekordów.

obejmuje - podczas korzystania z powiązania na każdym elemencie zestawu wyników.

Dłuższa wersja

Połączenia służą do filtrowania zestawu wyników pochodzących z bazy danych. Używasz go do wykonywania operacji ustawiania na stole. Pomyśl o tym jak o klauzuli where, która wykonuje zbiór teorii.

Post.joins(:comments)

jest taki sam jak

Post.where('id in (select post_id from comments)')

Tyle że jeśli jest więcej niż jeden komentarz, otrzymasz z powrotem duplikaty postów wraz z dołączeniami. Ale każdy post będzie postem z komentarzami. Możesz to poprawić, wyróżniając:

Post.joins(:comments).count
=> 10
Post.joins(:comments).distinct.count
=> 2

W umowie includesmetoda po prostu upewni się, że nie ma żadnych dodatkowych zapytań do bazy danych podczas odwoływania się do relacji (abyśmy nie zadawali n + 1 zapytań)

Post.includes(:comments).count
=> 4 # includes posts without comments so the count might be higher.

Morał polega na tym, aby używać, joinsgdy chcesz wykonywać operacje zestawu warunkowego, i używać, includesgdy zamierzasz używać relacji na każdym elemencie kolekcji.


To distinctmnie łapie za każdym razem. Dziękuję Ci!
Ben Hull,

4

.joins działa jako połączenie z bazą danych i łączy dwie lub więcej tabel i pobiera wybrane dane z zaplecza (bazy danych).

. obejmuje pracę jako lewe przyłączenie bazy danych. Załadował wszystkie rekordy lewej strony, nie ma znaczenia modelu po prawej stronie. Służy do szybkiego ładowania, ponieważ ładuje wszystkie skojarzone obiekty w pamięci. Jeśli wywołujemy skojarzenia przy uwzględnieniu wyniku zapytania, to nie uruchamia zapytania w bazie danych, po prostu zwraca dane z pamięci, ponieważ już załadowało dane do pamięci.


0

„złączenia” służyły tylko do łączenia tabel, a gdy wywoływano skojarzenia przy złączeniach, ponownie uruchomi zapytanie (oznacza to, że wiele zapytań zostanie uruchomionych)

lets suppose you have tow model, User and Organisation
User has_many organisations
suppose you have 10 organisation for a user 
@records= User.joins(:organisations).where("organisations.user_id = 1")
QUERY will be 
 select * from users INNER JOIN organisations ON organisations.user_id = users.id where organisations.user_id = 1

it will return all records of organisation related to user
and @records.map{|u|u.organisation.name}
it run QUERY like 
select * from organisations where organisations.id = x then time(hwo many organisation you have)

w tym przypadku całkowita liczba SQL wynosi 11

Ale z „włączeniem” chętnie załaduje dołączone skojarzenia i doda je do pamięci (załaduje wszystkie skojarzenia przy pierwszym załadowaniu) i nie uruchomi ponownie zapytania

gdy otrzymasz rekordy zawierające między innymi @ @ records = User.includes (: organizacje) .where („organisations.user_id = 1”), wówczas zapytanie będzie

select * from users INNER JOIN organisations ON organisations.user_id = users.id where organisations.user_id = 1
and 


 select * from organisations where organisations.id IN(IDS of organisation(1, to 10)) if 10 organisation
and when you run this 

@ records.map {| u | u.organisation.name} żadne zapytanie nie zostanie uruchomione

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.