Rails: include vs.: junções

345

Essa é mais uma pergunta "por que as coisas funcionam dessa maneira" em vez de uma pergunta "não sei como fazer isso" ...

Portanto, o evangelho sobre a obtenção de registros associados que você sabe que usará é o de usar, :includeporque você se unirá e evitará um monte de consultas extras:

Post.all(:include => :comments)

No entanto, quando você olha para os logs, não há associação acontecendo:

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) 

Ele está pegando um atalho porque ele puxa todos os comentários de uma vez, mas ainda não é uma junção (que é o que toda a documentação parece dizer). A única maneira de obter uma associação é usar em :joinsvez de :include:

Post.all(:joins => :comments)

E os logs mostram:

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

Estou esquecendo de algo? Eu tenho um aplicativo com meia dúzia de associações e, em uma tela, exibo dados de todas elas. Parece que seria melhor ter uma consulta ingressada em vez de 6 indivíduos. Sei que em termos de desempenho nem sempre é melhor fazer uma junção do que consultas individuais (na verdade, se você estiver gastando o tempo gasto, parece que as duas consultas individuais acima são mais rápidas que a junção), mas depois de todos os documentos Estou lendo, fico surpreso ao ver que :includenão está funcionando como anunciado.

Talvez o Rails esteja ciente do problema de desempenho e não participe, exceto em certos casos?

Rob Cameron
fonte
3
se você estava usando uma versão mais antiga do Rails, indique-o por meio de tags ou no corpo da sua pergunta. Caso contrário, se você estiver usando o Rails 4 NOW, ele será includes(para quem estiver lendo isso) #
1111 onebree
Também existe agora: preload e: eager_load blog.bigbinary.com/2013/07/01/…
CJW

Respostas:

179

Parece que a :includefuncionalidade foi alterada com o Rails 2.1. O Rails costumava fazer a junção em todos os casos, mas por razões de desempenho, foi alterado para usar várias consultas em algumas circunstâncias. Esta postagem no blog de Fabio Akita tem boas informações sobre a alteração (consulte a seção "Carregamento otimizado e ansioso").

Greg Campbell
fonte
Isso é muito útil, obrigado. Eu gostaria que, no entanto, houvesse uma maneira de forçar o Rails a fazer a junção, mesmo sem um 'onde' que exigisse. Em alguns casos, você sabe que a associação será mais eficiente e não correrá o risco de duplicação.
Jonathan Swartz
11
Veja também: blog.bigbinary.com/2013/07/01/…
Nathan Long
@JonathanSwartz Parece que a nova versão do Rails suporta isso usando o eagerload . Obrigado pelo link NathanLong
rubyprince
92

.joinsapenas junta as tabelas e traz os campos selecionados em troca. se você chamar associações no resultado da consulta de junções, ele acionará as consultas do banco de dados novamente

:includesansiosamente carregará as associações incluídas e as adicionará na memória. :includescarrega todos os atributos de tabelas incluídos. Se você chamar associações no resultado da consulta de inclusão, ela não acionará nenhuma consulta

Prem
fonte
71

A diferença entre junções e inclusão é que o uso da instrução include gera uma consulta SQL muito maior carregando na memória todos os atributos das outras tabelas.

Por exemplo, se você tiver uma tabela cheia de comentários e usar a: joins => users para extrair todas as informações do usuário para fins de classificação, etc. ela funcionará bem e levará menos tempo que: include, mas diga que deseja exibir o comentário junto com o nome do usuário, email, etc. Para obter as informações usando: joins, será necessário fazer consultas SQL separadas para cada usuário que buscar, enquanto que se você usou: include, essas informações estarão prontas para uso.

Ótimo exemplo:

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

holden
fonte
55

Recentemente, eu estava lendo mais sobre a diferença entre :joinse :includesnos trilhos. Aqui está uma explicação do que eu entendi (com exemplos :))

Considere este cenário:

  • Um usuário tem muitos comentários e um comentário pertence a um usuário.

  • O modelo de usuário possui os seguintes atributos: Nome (sequência), Idade (número inteiro). O modelo Comment possui os seguintes atributos: Content, user_id. Para um comentário, um user_id pode ser nulo.

Associa-se:

: joins executa uma junção interna entre duas tabelas. portanto

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

irá buscar todos os registros em que user_id (da tabela de comentários) seja igual a user.id (tabela de usuários). Assim, se você fizer

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

#=> <ActiveRecord::Relation []>

Você receberá uma matriz vazia, como mostrado.

Além disso, as junções não carregam a tabela unida na memória. Assim, se você fizer

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

Como você vê, comment_1.user.agedisparará uma consulta ao banco de dados novamente em segundo plano para obter os resultados

Inclui:

: includes executa uma junção externa esquerda entre as duas tabelas. portanto

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

resultará em uma tabela unida com todos os registros da tabela de comentários. Assim, se você fizer

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

ele buscará registros onde comments.user_id é nulo, como mostrado.

Além disso, inclui carrega as duas tabelas na memória. Assim, se você fizer

comment_1 = Comment.includes(:user).first

comment_1.user.age
#=> 24

Como você pode notar, o comment_1.user.age simplesmente carrega o resultado da memória sem disparar uma consulta ao banco de dados em segundo plano.

Aaditi Jain
fonte
Isso é para o Rails 4?
onebree
@HunterStevens: Sim, é #
Aaditi Jain
54

Além das considerações de desempenho, também há uma diferença funcional. Ao ingressar nos comentários, você está solicitando postagens que tenham comentários - uma associação interna por padrão. Quando você inclui comentários, solicita todas as postagens - uma associação externa.

Brian Maltzan
fonte
10

tl; dr

Eu os contraste de duas maneiras:

junções - para seleção condicional de registros.

inclui - Ao usar uma associação em cada membro de um conjunto de resultados.

Versão mais longa

Junções destina-se a filtrar o conjunto de resultados provenientes do banco de dados. Você o usa para fazer operações definidas na sua mesa. Pense nisso como uma cláusula where que executa a teoria dos conjuntos.

Post.joins(:comments)

é o mesmo que

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

Exceto que, se houver mais de um comentário, você receberá postagens duplicadas com as junções. Mas todas as postagens serão postadas com comentários. Você pode corrigir isso com distintas:

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

No contrato, o includesmétodo simplesmente garante que não haja consultas adicionais ao banco de dados ao fazer referência à relação (para que não façamos n + 1 consultas)

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

A moral é: use joinsquando quiser executar operações de conjunto condicional e use includesquando você estiver usando uma relação em cada membro de uma coleção.

Kevin Choubacha
fonte
Isso distinctme pega toda vez. Obrigado!
Ben Hull
4

.joins funciona como junção de banco de dados e junta duas ou mais tabelas e busca dados selecionados do back-end (banco de dados).

.inclui o trabalho como junção esquerda do banco de dados. Carregou todos os registros do lado esquerdo, não tem relevância do modelo do lado direito. É usado para carregamento rápido, pois carrega todos os objetos associados na memória. Se chamarmos associações no resultado da consulta de inclusão, ele não acionará uma consulta no banco de dados. Ele simplesmente retorna dados da memória porque já carregou dados na memória.


fonte
0

'joins' é usado apenas para juntar tabelas e, quando você chama associações em joins, ele aciona novamente a consulta (significa que muitas consultas são acionadas)

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)

número total de SQL é 11 neste caso

Porém, com 'includes', as associações incluídas serão carregadas com entusiasmo e as adicionarão na memória (carregará todas as associações na primeira carga) e não acionará a consulta novamente

Quando você obtém registros com include como @ records = User.includes (: organisations) .where ("organisations.user_id = 1"), a consulta será

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} nenhuma consulta será acionada

Thorin
fonte