Rails: inclure vs: jointures

345

C'est plus une question "pourquoi les choses fonctionnent de cette façon" plutôt qu'une question "je ne sais pas comment faire" ...

Donc, l'évangile sur l'extraction des enregistrements associés que vous savez que vous allez utiliser est d'utiliser :includeparce que vous obtiendrez une jointure et évitez tout un tas de requêtes supplémentaires:

Post.all(:include => :comments)

Cependant, lorsque vous regardez les journaux, aucune jointure ne se produit:

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) 

Il est prend un raccourci car il tire tous les commentaires à la fois, mais il est toujours pas une jointure (qui est ce que toute la documentation semble dire). La seule façon d'obtenir une jointure est d'utiliser à la :joinsplace de :include:

Post.all(:joins => :comments)

Et les journaux montrent:

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

Suis-je en train de manquer quelque chose? J'ai une application avec une demi-douzaine d'associations et sur un écran, j'affiche les données de chacune d'entre elles. Il semble qu'il serait préférable d'avoir une requête jointe au lieu de 6 individus. Je sais qu'en termes de performances, il n'est pas toujours préférable de faire une jointure plutôt que des requêtes individuelles (en fait, si vous passez par le temps passé, il semble que les deux requêtes individuelles ci-dessus soient plus rapides que la jointure), mais après tous les documents J'ai lu que je suis surpris de :includene pas travailler comme annoncé.

Peut-être que Rails est conscient du problème de performances et ne se joint pas, sauf dans certains cas?

Rob Cameron
la source
3
si vous utilisiez une ancienne version de Rails, veuillez l'indiquer via des balises ou dans le corps de votre question. Sinon, si vous utilisez Rails 4 NOW, c'est includes(pour tous ceux qui liront ceci)
onebree
Il y a aussi maintenant: précharge et: eager_load blog.bigbinary.com/2013/07/01/…
CJW

Réponses:

179

Il semble que la :includefonctionnalité a été modifiée avec Rails 2.1. Dans tous les cas, Rails effectuait la jointure, mais pour des raisons de performances, il a été modifié pour utiliser plusieurs requêtes dans certaines circonstances. Ce billet de blog de Fabio Akita contient de bonnes informations sur le changement (voir la section intitulée "Chargement optimisé et désireux").

Greg Campbell
la source
C'est très utile, merci. Je souhaite cependant qu'il y ait un moyen de forcer Rails à faire la jointure même sans «où» cela l'exige. Dans certains cas, vous savez que la jointure sera plus efficace et n'encourra pas de risque de duplication.
Jonathan Swartz
1
Voir aussi: blog.bigbinary.com/2013/07/01/…
Nathan Long
@JonathanSwartz On dirait que la nouvelle version Rails supporte cela en utilisant eagerload . Merci pour le lien NathanLong
rubyprince
92

.joinsrejoint simplement les tables et apporte en retour les champs sélectionnés. si vous appelez des associations sur le résultat de la requête de jointure, il lancera à nouveau les requêtes de base de données

:includessera impatient de charger les associations incluses et de les ajouter en mémoire. :includescharge tous les attributs de tables inclus. Si vous appelez des associations sur le résultat de la requête d'inclusion, il ne déclenchera aucune requête

Prem
la source
71

La différence entre les jointures et les inclusions est que l'utilisation de l'instruction include génère une requête SQL beaucoup plus volumineuse chargeant en mémoire tous les attributs des autres tables.

Par exemple, si vous avez un tableau plein de commentaires et que vous utilisez un: joins => utilisateurs pour extraire toutes les informations utilisateur à des fins de tri, etc. cela fonctionnera bien et prendra moins de temps que: inclure, mais dites que vous voulez afficher le commentaire ainsi que le nom de l'utilisateur, l'adresse e-mail, etc. Pour obtenir les informations à l'aide de: jointures, il devra effectuer des requêtes SQL distinctes pour chaque utilisateur qu'il récupère, alors que si vous avez utilisé: include, ces informations sont prêtes à l'emploi.

Excellent exemple:

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

holden
la source
55

Je lisais récemment plus sur la différence entre :joinset :includesdans les rails. Voici une explication de ce que j'ai compris (avec des exemples :))

Considérez ce scénario:

  • Un utilisateur a_de nombreux commentaires et un commentaire appartient_ à un utilisateur.

  • Le modèle utilisateur a les attributs suivants: nom (chaîne), âge (entier). Le modèle Comment a les attributs suivants: Content, user_id. Pour un commentaire, un user_id peut être nul.

Joint:

: join effectue une jointure interne entre deux tables. Donc

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

va récupérer tous les enregistrements où user_id (de la table des commentaires) est égal à user.id (table des utilisateurs). Ainsi, si vous le faites

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

#=> <ActiveRecord::Relation []>

Vous obtiendrez un tableau vide comme indiqué.

De plus, join ne charge pas la table jointe en mémoire. Ainsi, si vous le faites

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

Comme vous le voyez, comment_1.user.agelancera à nouveau une requête de base de données en arrière-plan pour obtenir les résultats

Comprend:

: includes effectue une jointure externe gauche entre les deux tables. Donc

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

entraînera une table jointe avec tous les enregistrements de la table des commentaires. Ainsi, si vous le faites

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

il récupérera les enregistrements où comments.user_id est nul comme indiqué.

De plus, il charge les deux tables en mémoire. Ainsi, si vous le faites

comment_1 = Comment.includes(:user).first

comment_1.user.age
#=> 24

Comme vous pouvez le constater, comment_1.user.age charge simplement le résultat de la mémoire sans lancer une requête de base de données en arrière-plan.

Aaditi Jain
la source
Est-ce pour Rails 4?
onebree
@HunterStevens: Oui c'est ça
Aaditi Jain
54

En plus des considérations de performances, il existe également une différence fonctionnelle. Lorsque vous joignez des commentaires, vous demandez des articles qui ont des commentaires - une jointure interne par défaut. Lorsque vous incluez des commentaires, vous demandez tous les articles - une jointure externe.

Brian Maltzan
la source
10

tl; dr

Je les contraste de deux manières:

jointures - Pour la sélection conditionnelle des enregistrements.

comprend - Lors de l'utilisation d'une association sur chaque membre d'un jeu de résultats.

Version plus longue

Les jointures sont destinées à filtrer l'ensemble de résultats provenant de la base de données. Vous l'utilisez pour effectuer des opérations de définition sur votre table. Considérez cela comme une clause where qui exécute la théorie des ensembles.

Post.joins(:comments)

est le même que

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

Sauf que s'il y a plus d'un commentaire, vous obtiendrez des articles en double avec les jointures. Mais chaque article sera un article avec des commentaires. Vous pouvez corriger cela avec distinct:

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

En contrat, la includesméthode s'assurera simplement qu'il n'y a pas de requêtes de base de données supplémentaires lors du référencement de la relation (afin que nous ne fassions pas n + 1 requêtes)

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

La morale est, à utiliser joinslorsque vous souhaitez effectuer des opérations d'ensemble conditionnelles et à utiliser includeslorsque vous allez utiliser une relation sur chaque membre d'une collection.

Kevin Choubacha
la source
Cela distinctme fait à chaque fois. Je vous remercie!
Ben Hull
4

.joins fonctionne comme une jointure de base de données et joint deux ou plusieurs tables et récupère les données sélectionnées du backend (base de données).

.comprend le travail comme jointure gauche de la base de données. Il a chargé tous les enregistrements du côté gauche, n'a pas la pertinence du modèle du côté droit. Il est utilisé pour un chargement rapide car il charge tous les objets associés en mémoire. Si nous appelons des associations sur le résultat de la requête d'inclusion, cela ne déclenche pas de requête sur la base de données, il renvoie simplement les données de la mémoire car il a déjà chargé des données en mémoire.


la source
0

'jointures' juste utilisé pour joindre des tables et lorsque vous avez appelé des associations sur des jointures, il lancera à nouveau la requête (cela signifie que de nombreuses requêtes se déclencheront)

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)

le nombre total de SQL est de 11 dans ce cas

Mais avec 'includes', vous souhaiterez charger les associations incluses et les ajouter en mémoire (charger toutes les associations lors du premier chargement) et ne pas lancer de requête à nouveau

lorsque vous obtenez des enregistrements avec des inclus comme @ records = User.includes (: organizations) .where ("organisations.user_id = 1") alors la requête sera

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} aucune requête ne se déclenchera

Thorin
la source