En estos ejemplos la Tabla A tiene un has_many
y la Tabla B un belongs_to
y no tienen en cuenta que en la Tabla B podría haber varias referencias al mismo registro de la Tabla A, lo que produce duplicados de los registros de la Tabla A al hacer los joins.
- INNER JOIN -> JOIN
- LEFT JOIN -> LEFT OUTER JOIN
- A RIGHT JOIN -> RIGHT OUTER JOIN
- FULL JOIN -> FULL OUTER JOIN
ActiveRecord::Schema[7.0].define(version: 2022_07_11_100031) do
create_table "comments", force: :cascade do |t|
t.integer "post_id"
t.text "text"
t.string "status"
t.index ["post_id"], name: "index_comments_on_post_id"
end
create_table "posts", force: :cascade do |t|
t.string "title"
t.string "status"
end
end
Post.create(title: 'Post 1', status: 'published').tap do |post|
post.comments.create(text: 'Comment 1 in Post 1', status: 'approved')
post.comments.create(text: 'Comment 2 in Post 1', status: 'approved')
post.comments.create(text: 'Comment 3 in Post 1', status: 'pending')
end
Post.create(title: 'Post 2', status: 'draft').tap do |post|
post.comments.create(text: 'Comment 1 in Post 2', status: 'approved')
post.comments.create(text: 'Comment 2 in Post 2', status: 'rejected')
post.comments.create(text: 'Comment 3 in Post 2', status: 'pending')
end
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end
class Comment < ApplicationRecord
belongs_to :post
end
Nos permite ahorrar consultas adicionales al traernos todos los datos que necesitamos cuando trabajamos con modelos relacionados.
Se usa para precargar, comparar y filtrar datos con datos asociados.
- Usa
LEFT OUTER JOIN
para cargar los datos asociados. - Resuelve el problema de las consultas N+1
- Al usar
LEFT OUTER JOIN
, los datos asociados pueden no ser todos los que realmente existen si han sido filtrados en la consulta.
Siempre que vayamos a hacer uso de los datos de la tabla relacionada, por ejemplo consultando sus campos, ya que evita el problema del N+1.
Cuando no usamos includes
:
Post.all.each do |post|
post.comments.each do |comment|
comment.text
end
end
SELECT "posts".* FROM "posts"
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 2
Cuando usamos includes
en su forma básica, sólo ejecuta dos consultas:
posts = Post.includes(:comments)
posts.each do |post|
post.comments.each do |comment|
comment.text
end
end
SELECT "posts".* FROM "posts"
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2)
Si añado la claúsulawhere
para filtrar por datos del modelo Post, sigue usando dos consultas separadas para obtener los datos:
Post.includes(:comments).where(status: :published)
SELECT "posts".* FROM "posts" WHERE "posts"."status" = "published"
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
Si añado la claúsulawhere
para filtrar por datos del modelo Comment, entonces Rails construye una consulta mediante el uso de LEFT OUTER JOIN
:
Post.includes(:comments).where(comments: { status: :approved })
SELECT "posts"."id" AS t0_r0,
"posts"."title" AS t0_r1,
"posts"."status" AS t0_r2,
"comments"."id" AS t1_r0,
"comments"."post_id" AS t1_r1,
"comments"."text" AS t1_r2,
"comments"."status" AS t1_r3
FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
WHERE "comments"."status" = "approved"
Cuando se usa
includes
, hay que usar los nombres de las relaciones.
Nos permite cargar la asociación usando una sóla consulta con LEFT OUTER JOIN
. Esto es exactamente lo mismo que hace includes
cuando se ve forzada a usar una sóla consulta al aplicar un where
o un order
sobre la tabla que se está incluyendo.
Post.eager_load(:comments)
SELECT "posts"."id" AS t0_r0,
"posts"."title" AS t0_r1,
"posts"."status" AS t0_r2,
"comments"."id" AS t1_r0,
"comments"."post_id" AS t1_r1,
"comments"."text" AS t1_r2,
"comments"."status" AS t1_r3
FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
Carga los datos asociados usando una consulta separada. Es exactamente lo mismo que hace includes
en su forma básica.
Post.preload(:comments)
SELECT "posts".* FROM "posts"
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2)
En este caso no podemos usar la cláusula
where
para filtrar los datos decomments
, aunque si que podemos usarla para los datos deposts
.
Realiza un INNER JOIN
entre las dos tablas. Se usa para comparar y filtrar datos usando los datos asociados.
- Hace uso de
INNER JOIN
para cargar los datos asociados, por lo que los datos asociados no se ven afectados. Si un post tiene 12 comentarios, se devolverán los 12 comentarios al hacer unposts.first.comments.length
.
- No resuelve el problema del N+1 ya que no almacena los datos asociados. Se puede resolver aplicando un
preload
.Post.joins(:comments).where(comments: { status: "approved" }).preload(:comments)
- Carga los datos en dos consultas separadas, cargando primero los datos del post y luego los comentarios.
- Puede arrojar datos duplicados. Se puede resolver aplicando
distinct
.Post.joins(:comments).select('distinct posts.*') # OR Post.joins(:comments).distinct
Cuando sólo necesitemos filtrar u ordenar por datos de la tabla relacionada pero no usarlos. Hay que tener en cuenta que puede tener registros duplicados, para lo cual deberemos hacer uso del distinct.
posts = Post.joins(:comments)
posts.each do |post|
post.comments.each do |comment|
comment.text
end
end
SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 2
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 2
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 2
En este caso además se estaría duplicando las filas que se devuelven para los posts, ya que por cada comentario que tiene un post, se devuelve duplicado el post:
Post.joins(:comments)
[
#<Post:0x000000010ad0e5c0 id: 1, title: "Post 1", status: "published">,
#<Post:0x000000010ad0e390 id: 1, title: "Post 1", status: "published">,
#<Post:0x000000010ad0dd00 id: 1, title: "Post 1", status: "published">,
#<Post:0x000000010ad0d878 id: 2, title: "Post 2", status: "draft">,
#<Post:0x000000010ad0ce00 id: 2, title: "Post 2", status: "draft">,
#<Post:0x000000010ad07900 id: 2, title: "Post 2", status: "draft">
]
Operación | Uso de join | Carga en memoria de las asociaciones | Uso de dos consultas |
---|---|---|---|
joins | si (inner join) | no | no |
preload | no | si | si |
includes | si (left join) | si | depende |
eager_load | si (left join) | si | no |
- https://dev.to/delbetu/join-vs-includes-vs-eager-load-vs-preload-36l3
- https://tadhao.medium.com/joins-vs-preload-vs-includes-vs-eager-load-in-rails-5f721c44b3a9
- https://www.bigbinary.com/blog/preload-vs-eager-load-vs-joins-vs-includes
- https://scoutapm.com/blog/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where