Rails: включить против: соединения

345

Это скорее вопрос «почему все работает так», а не вопрос «я не знаю, как это сделать» ...

Таким образом, Евангелие при извлечении связанных записей, которое, как вы знаете, вы собираетесь использовать, заключается в том, чтобы использовать его, :includeпотому что вы получите соединение и избежите целого ряда дополнительных запросов:

Post.all(:include => :comments)

Однако, когда вы просматриваете журналы, никакого объединения не происходит:

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) 

Он будет принимать ярлык , потому что он тянет все комментарии сразу, но все-таки не присоединиться (что вся документация , кажется, говорят). Единственный способ получить объединение - использовать :joinsвместо :include:

Post.all(:joins => :comments)

И логи показывают:

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

Я что-то упускаю? У меня есть приложение с полдюжиной ассоциаций, и на одном экране я отображаю данные по всем из них. Похоже, было бы лучше иметь один объединенный запрос вместо 6 отдельных. Я знаю, что с точки зрения производительности не всегда лучше выполнять объединение, а не отдельные запросы (на самом деле, если вы тратите время, похоже, что два вышеупомянутых отдельных запроса выполняются быстрее, чем объединение), но после всех документов Я читал, я удивлен, увидев :includeне работает, как рекламируется.

Может быть , Rails является сознают проблемы производительности и не присоединяется , за исключением некоторых случаев?

Роб Кэмерон
источник
3
если вы использовали более старую версию Rails, укажите это с помощью тегов или в теле вопроса. В противном случае, если вы используете Rails 4 СЕЙЧАС, это includes(для тех, кто читает это)
onebree
Также есть сейчас: preload и: eager_load blog.bigbinary.com/2013/07/01/…
CJW

Ответы:

179

Похоже, что :includeфункциональность была изменена с Rails 2.1. Rails использовался для объединения во всех случаях, но по соображениям производительности в некоторых случаях было изменено использование нескольких запросов. Этот пост в блоге Фабио Акиты содержит полезную информацию об изменениях (см. Раздел «Оптимизированная загрузка с нетерпением»).

Грег Кэмпбелл
источник
2
Смотрите: samsaffron.com/archive/2008/03/15/...
Sam Saffron
Это очень полезно, спасибо. Я хотел бы, однако, чтобы был способ заставить Rails выполнять объединение даже без «где», которое этого требует. В некоторых случаях вы знаете, что объединение будет более эффективным и не повлечет за собой риск дублирования.
Джонатан Шварц
1
Смотрите также: blog.bigbinary.com/2013/07/01/…
Натан Лонг
@JonathanSwartz Похоже, что новая версия Rails поддерживает это с помощью eagerload . Спасибо за ссылку НатанЛонг
rubyprince
92

.joinsпросто присоединяется к таблицам и возвращает выбранные поля. если вы вызовете ассоциации для результата запроса объединений, он снова запустит запросы к базе

:includesбудет стремиться загрузить включенные ассоциации и добавить их в память. :includesзагружает все включенные атрибуты таблиц. Если вы вызываете ассоциации на результат запроса включения, он не будет запускать какие-либо запросы

Prem
источник
71

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

Например, если у вас есть таблица, заполненная комментариями, и вы используете: joins => users для извлечения всей информации о пользователях для целей сортировки и т. Д., То она будет работать нормально и займет меньше времени, чем: include, но, скажем, вы хотите отобразить комментарий вместе с именем пользователя, адресом электронной почты и т. д. Чтобы получить информацию с помощью: объединений, он должен будет выполнять отдельные запросы SQL для каждого пользователя, которого выбирает, тогда как если вы использовали: include, эта информация готова к использованию.

Отличный пример:

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

удержаны
источник
55

Недавно я читал больше о разнице между :joinsи :includesв рельсах. Вот объяснение того, что я понял (с примерами :))

Рассмотрим этот сценарий:

  • Пользователь has_many комментарии и комментарий принадлежит_ к пользователю.

  • Модель User имеет следующие атрибуты: Имя (строка), Возраст (целое число). Модель Comment имеет следующие атрибуты: Content, user_id. Для комментария user_id может быть нулевым.

Соединения:

: joins выполняет внутреннее соединение двух таблиц. таким образом

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">]>

извлечет все записи, где user_id (из таблицы комментариев) равен user.id (таблица пользователей). Таким образом, если вы делаете

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

#=> <ActiveRecord::Relation []>

Вы получите пустой массив, как показано на рисунке.

Более того, объединения не загружают объединенную таблицу в память. Таким образом, если вы делаете

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

Как вы видите, comment_1.user.ageзапрос базы данных снова будет запущен в фоновом режиме, чтобы получить результаты

Включает:

: включает выполнение левого внешнего соединения между двумя таблицами. таким образом

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">]>

приведет к объединенной таблице со всеми записями из таблицы комментариев. Таким образом, если вы делаете

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">]>

он будет получать записи, где comments.user_id равен nil, как показано.

Причем включает в себя загружает обе таблицы в память. Таким образом, если вы делаете

comment_1 = Comment.includes(:user).first

comment_1.user.age
#=> 24

Как вы можете заметить, comment_1.user.age просто загружает результат из памяти, не отправляя запрос к базе данных в фоновом режиме.

Аадити Джайн
источник
Это для Rails 4?
августа
@HunterStevens: Да, это так
Аадити Джайн
54

В дополнение к соображениям производительности, есть и функциональная разница. Когда вы присоединяетесь к комментариям, вы запрашиваете посты с комментариями - внутреннее объединение по умолчанию. Когда вы включаете комментарии, вы запрашиваете все сообщения - внешнее объединение.

Брайан Мальцан
источник
10

ТЛ; др

Я противопоставляю их двумя способами:

присоединяется - для условного отбора записей.

includes - При использовании ассоциации для каждого члена набора результатов.

Более длинная версия

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

Post.joins(:comments)

такой же как

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

За исключением того, что если есть более одного комментария, вы получите повторяющиеся посты обратно с объединениями. Но каждый пост будет постом с комментариями. Вы можете исправить это с помощью:

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

В контракте includesметод просто гарантирует, что при обращении к связи нет дополнительных запросов к базе данных (чтобы мы не делали n + 1 запросов)

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

Мораль такова: используйте, joinsкогда вы хотите выполнить операции условного набора, и используйте, includesкогда вы собираетесь использовать отношение для каждого члена коллекции.

Кевин Чубача
источник
Это distinctполучает меня каждый раз. Спасибо!
Бен Халл
4

.joins работает как объединение базы данных и объединяет две или более таблицы и извлекает выбранные данные из серверной части (базы данных).

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


источник
0

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

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)

общее количество SQL в этом случае 11

Но с помощью «include» будет стремиться загрузить включенные ассоциации и добавить их в память (загрузить все ассоциации при первой загрузке) и не запускать запрос снова

когда вы получаете записи с помощью таких включений, как @ records = User.include (: organization) .where ("organisations.user_id = 1"), тогда запрос будет

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} ни один запрос не будет запущен

Торин
источник