[Rails] joins vs preload vs includes vs eager_load의 차이점

Jinsu Kim·2023년 10월 2일
0

rails

목록 보기
6/7
post-thumbnail

다른 엔지니어와 이야기하던 도중 includes와 joins의 차이가 뭔가요? 라는 질문을 들었는데, 잘 이해하고 있다고 생각했지만 실제로 찾아보니 1도 이해하고 있지 않아 정리하고자 합니다.

Rails의 ActiveRecord에 N+1 문제를 해결하려 했는데 가장 많이 사용하는 것은 includes, preload, eager_load인 것 같습니다.

Joins

그래도 관련 모든 메소드를 정리해보기 위해 먼저 joins메소드 부터 알아가보도록 하겠습니다. 먼저 아래의 예시를 보져

User.joins(:posts).where(posts: { id: 1 })
# SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1

디폴트로 INNER JOIN을 사용하는 것을 확인 할 수 있습니다. LEFT OUTER JOIJ을 사용할 경우 left_joins를 사용합니다.

위에 다른 세가지 메소드와 다른점은 association을 캐쉬 하지않는 것입니다.
이게 생각보다 중요한 포인트가 될 수 있을 것 같은데, 캐쉬를 하고 안하고는 성능에서도 영향을 끼칠 수 있으니 이점 주의하시면 좋을 것 같습니다.

장점으로서는 캐쉬를 사용하지 않기 때문에 메모리를 사용하지 않는 점입니다.
따라서 JOIN을 사용해서 조건 범위를 축소시키고 싶지만 JOIN한 테이블의 데이터를 사용하지 않는 경우는 joins 메소드를 사용하는 것이 좋습니다.

아래와 사용예시 입니다

User.joins(*).where(posts: {*})
User.joins(*).merge(Post.*)

eager_load

User.eager_load(:posts)
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id`

User.eager_load(:posts).where(posts: { id: 1 })
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1

지정한 association을 LEFT OUTER JOIN으로 뽑아 캐시한다.

장점으로는

  • 쿼리 수가 1개면 되므로 경우에 따라서는 preload보다 빠르다.
  • 조인하고 있기 때문에 preload와 달리 joins와 마찬가지로 조인한 테이블에서 범위를 좁힐 수 있다.

preload

User.preload(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)

User.preload(:posts).where(posts: { id: 1 })
# SELECT `users`.* FROM `users`  WHERE `posts`.`id` = 1
# => Mysql2::Error: Unknown column 'posts.id' in 'where clause': SELECT `users`.* FROM `users`  WHERE `posts`.`id` = 1

지정한 association을 여러 쿼리로 나눠서캐시한다.

위의 eager_load와의 명확한 차이점인 것 같습니다.
근데 어떨 때 사용할까요? 여러 개의 association을 eager loading 할 때나 별로 JOIN하고 싶지 않은 큰 테이블을 다룰 때는 preload를 사용하는 것이 좋을 것 같습니다.
preload 한 테이블에 의해 조건을 걸면 에러를 발생시킵니다.

includes

User.includes(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)

User.includes(:posts).where(posts: { id: 1 })
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1
  • includes 한 테이블에서 where에 의한 필터링을 실시하고 있다
  • includes한 association에 대해 joins나 references도 부르고 있다.
  • 임의의 association에 대해서 eager_load도 부르고 있다.

위의 경우 중 하나를 만족시킬 경우 eager_load와 동일한 LEFT JOIN을 수행합니다

그렇지 않으면 preload와 동일하게 쿼리를 나누어 실행하는 것이 특징입니다.
또 하나의 특징이 있다면
필터링이 필요 할 때 예외를 던지지 않고 eager_load에 fallback하는 preload입니다

실제 구현되어 있는 코드

def eager_loading?
  @should_eager_load ||=
    eager_load_values.any? ||
    includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end

정리

메소드캐쉬쿼리용도
joinsX단수조건 설정
eager_loadO단수캐쉬, 조건 설정
preloadO복수캐쉬
includesO경우에 따라 다름캐쉬, 필요하면 조건 설정

해당 테이블과의 JOIN을 하지않고 싶은 경우에는 preload
JOIN을 해도 문제가 없고 일단 eager loading을 하려면 includes
반드시 JOIN을 하려면 eager_load를 사용하면 좋을 것 같습니다.

회사에 따라서는 joins, includes로 통일 되어있는 회사도 있을 것 같으니 회사의 룰에 따라 가면 될 것 같습니다!(좀 더 퍼포먼스 튜닝을 하고 싶다면 경우에 따라 다르게 사용하면 good)

참고 사이트

profile
Ruby와 js로 첫 커리어를 시작하였고 3년차 엔진니어입니다! vim에 관심이 많습니다!

0개의 댓글