Ruby On Rails에서 N+1 문제를 해결할 수 있는 방법에 대해 알아보려고 한다.
N+1 문제란?
연관 관계가 설정된 엔티티를 조회할 경우 조회된 데이터 개수(n)만큼 연관 관계의 조회 쿼리가 추가로 실행되는 것을 의미한다.
School 테이블과 Teacher 테이블이 1:N 관계라고 가정해보자.
class School < ApplicationRecord
has_many: :teachers
end
class Teacher < ApplicationRecord
belongs_to: :school
end
각 학교별 선생님들의 이름을 출력하고자 했을 때,
School 데이터 전체를 조회하는 쿼리 하나와 각각의 school에 속해 있는 teachers 데이터를 조회하는 쿼리가 추가로 발생하는 것을 볼 수 있다.
School.all.each do |school|
school.teachers.each do |teacher|
puts "#{school.name} : #{teacher.name}"
end
end
> SELECT `schools`.* FROM `schools` ORDER BY `schools`.`id`
> SELECT `teachers`.* FROM `teachers` WHERE `teachers`.`school_id` = 1
> SELECT `teachers`.* FROM `teachers` WHERE `teachers`.`school_id` = 2
> SELECT `teachers`.* FROM `teachers` WHERE `teachers`.`school_id` = 3
> SELECT `teachers`.* FROM `teachers` WHERE `teachers`.`school_id` = 4
> SELECT `teachers`.* FROM `teachers` WHERE `teachers`.`school_id` = 5
이러한 N+1 문제를 피할 수 있는 2가지 방법은 아래와 같다.
- 연관 관계를 가져오기 위해 큰 조인 기반의 쿼리를 만듦
- 연관 관계별로 별도의 쿼리를 만듦
Ruby On Rails에서는 Eager loading을 통해 N+1 쿼리 문제를 해결할 수 있다.
preload
연관 관계에 있는 테이블별로 별도의 쿼리가 생성된다. (테이블 당 하나의 쿼리)
School.preload(:teachers)
> SELECT `schools`.* FROM `schools`
> SELECT `teachers`.* FROM `teachers` WHERE `teachers`.`school_id` IN (1,2,3,4,5)
별도로 쿼리가 만들어지기 때문에 아래와 같이 연결 관계의 테이블에 관한 조건절(where, order by)은 사용할 수 없다.
School.preload(:teachers).where('teachers.name=?', '홍길동')
그러나 선행 모델에 관한 조건절 사용은 가능하다.
School.preload(:teaches).where('schools.name=?', '루비 고등학교')
eager_load
LEFT OUTER JOIN을 사용하여 단일 쿼리로 모든 연결 관계를 로드한다.
단일 쿼리로 조회하기 때문에 타 테이블을 참고하는 조건절 사용이 가능하다.
School.eager_load(:teachers).where('teachers.name=?', '홍길동').to_a
SELECT "schools"."id" AS t0_r0, "schools"."name" AS t0_r1, "teachers"."id" AS t1_r0,
"teachers"."name" AS t1_r1, "teachers"."school_id" AS t1_r2
FROM "schools" LEFT OUTER JOIN "teachers" ON "teachers"."school_id" = "schools"."id"
WHERE "teachers"."name" = "홍길동"
includes
기본 동작은 preload와 같이 별도의 쿼리에서 연결 관계를 로드한다.
School.includes(:teachers)
> SELECT `schools`.* FROM `schools`
> SELECT `teachers`.* FROM `teachers` WHERE `teachers`.`school_id` IN (1,2,3,4,5)
preload와 차이점은 타 테이블의 조건절을 추가했을 때 자동으로 eager_load와 같이 단일 쿼리로 동작하게 된다.
또한, 어떠한 이유로 단일 쿼리를 사용하도록 강제하고 싶다면 조건절 추가 없이도 references를 통해 단일 쿼리로 실행할 수 있다.
School.includes(:teachers).where('teachers.name = ?', '홍길동')
> SELECT "schools"."id" AS t0_r0, "schools"."name" AS t0_r1, "teachers"."id" AS t1_r0,
"teachers"."name" AS t1_r1, "teachers"."school_id" AS t1_r2
FROM "schools" LEFT OUTER JOIN "teachers" ON "teachers"."school_id" = "schools"."id"
WHERE "teachers"."name" = "홍길동"
School.includes(:teachers).references(:teachers)
> SELECT "schools"."id" AS t0_r0, "schools"."name" AS t0_r1, "teachers"."id" AS t1_r0,
"teachers"."name" AS t1_r1, "teachers"."school_id" AS t1_r2
FROM "schools" LEFT OUTER JOIN "teachers" ON "teachers"."school_id" = "schools"."id"
'Programming > Ruby On Rails' 카테고리의 다른 글
[Ruby On Rails] Module Mixin (2) - ActiveSupport::Concern (0) | 2023.04.30 |
---|---|
[Ruby On Rails] Module Mixin (1) - include, prepend, extend (0) | 2023.04.18 |
[Ruby On Rails] Elasticsearch 검색 템플릿(Search Template) 사용하기 (0) | 2023.03.28 |
[Ruby On Rails] enum 경고 문구 - Overwriting existing method (0) | 2023.03.26 |
[Ruby On Rails] 동시성 제어하기 - ActiveRecord Locking (0) | 2023.03.22 |