ActiveRecord의 N+1 질문 및 (proeload, eager load, includes, joins)
개시하다
액티브 레코드에서 여러 테이블을 넘나드는 검색을 할 때 N+1 문제를 해결하기 위해 사용된 기사
includes
를 자주 보고 이를 참고했지만, 그다지 좋지 않다고 해 각종 조사 결과를 정리한 메모다.문제에 관하여
순환 처리 중 매번 SQL을 발행하고 대량의 SQL을 발행하여 성능이 떨어지는 문제를 가리킨다.
ActiveRecord와 같은 OR Mapper를 사용할 경우 발생하기 쉽습니다.
왜 일어났을까
읽기 지연이 가능한 ActiveRecord의 기본값입니다.관련 양식이 필요할 때, 검색어를 발행하고 필요한 값을 꺼냅니다.
메모리를 확보하는 양은 적지만 관련 표를 사용할 때마다 SQL이 발매되어 동작이 무거워진다.
예를 들어 어떤 일람표를 만들 때
해결책
대량 읽기라고도 부른다.
사전에 관련 표를 모두 메모리에 저장했음을 나타낸다.
조회가 적어도 되기 때문에 묘사가 빨라지지만 관련 표도 모두 메모리에 있기 때문에 해당하는 메모리를 소모한다.
건수를 줄여서 사용하는 것이 좋다.
사용
preload
,eager_load
,includes
등의 방법.환경 확인
책상 사이의 관계
각 버전
Ruby
Rails(ActiveRecord)
MySQL
2.7.1
6.0.3.1
5.6
joins
User.joins(:posts).where(posts: { id: 1 })
# User Load (2.4ms) SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1 LIMIT 11
Post.joins(:user).where(users: {name: 'user1' })
# Post Load (2.6ms) SELECT `posts`.* FROM `posts` INNER JOIN `users` ON `users`.`id` = `posts`.`user_id` WHERE `users`.`name` = 'user1' LIMIT 11
테이블을 INNER JOIN으로 연결합니다.LEFT OUTER JOIN을 사용할 경우left_joins
.JOIN 문장을 만드는 조회일 뿐이기 때문에 캐시 연결을 하지 않기 때문에 N+1 문제가 발생하지만 메모리의 소비를 억제할 수 있다.
따라서 JOIN 테이블의 데이터를 사용하지 않고 압축
where
,order
joins
를 추천합니다.preload
User.preload(:posts)
# User Load (2.8ms) SELECT `users`.* FROM `users` LIMIT 11
# Post Load (2.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
Post.preload(:user)
# Post Load (3.0ms) SELECT `posts`.* FROM `posts` LIMIT 11
# User Load (1.9ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (28, 22, 40, 5, 41, 1, 23, 11, 34, 47, 15)
지정한 관련 표에 대한 조회를 실행하고 가져오고 캐시합니다.JOIN을 통한 표 결합이 아니기 때문에
preload
테이블에서 축소할 수 없으며 예외를 던질 수 있다.여러 연관이 완료된 후 또는 큰 테이블을 처리하는 등 JOIN을 하기 싫은 상태에서 사용한다.
조회에는 IN 문장이 있는데 메인 테이블에 기록된 숫자가 많은 경우 IN 문장이 팽창해 MySQL 등 데이터베이스 오류를 일으킬 수 있으므로 페이지 등으로 건수를 줄이는 것이 좋다.
eager_load
User.eager_load(:posts)
# SQL (2.5ms) SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` LIMIT 11
# SQL (3.2ms) 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`.`content` AS t1_r1, `posts`.`user_id` AS t1_r2, `posts`.`created_at` AS t1_r3, `posts`.`updated_at` AS t1_r4 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
User.eager_load(:posts).where(posts: { id: 1 })
# SQL (2.5ms) SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1 LIMIT 11
# SQL (1.7ms) 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`.`content` AS t1_r1, `posts`.`user_id` AS t1_r2, `posts`.`created_at` AS t1_r3, `posts`.`updated_at` AS t1_r4 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1 AND `users`.`id` = 10
Post.eager_load(:user)
# SQL (1.9ms) SELECT `posts`.`id` AS t0_r0, `posts`.`content` AS t0_r1, `posts`.`user_id` AS t0_r2, `posts`.`created_at` AS t0_r3, `posts`.`updated_at` AS t0_r4, `users`.`id` AS t1_r0, `users`.`name` AS t1_r1, `users`.`created_at` AS t1_r2, `users`.`updated_at` AS t1_r3 FROM `posts` LEFT OUTER JOIN `users` ON `users`.`id` = `posts`.`user_id` LIMIT 11
Post.eager_load(:user).where(users: { name: 'user1' })
# SQL (2.3ms) SELECT `posts`.`id` AS t0_r0, `posts`.`content` AS t0_r1, `posts`.`user_id` AS t0_r2, `posts`.`created_at` AS t0_r3, `posts`.`updated_at` AS t0_r4, `users`.`id` AS t1_r0, `users`.`name` AS t1_r1, `users`.`created_at` AS t1_r2, `users`.`updated_at` AS t1_r3 FROM `posts` LEFT OUTER JOIN `users` ON `users`.`id` = `posts`.`user_id` WHERE `users`.`name` = 'user1' LIMIT 11
LEFT OUTER JOIN이 지정한 데이터를 병합하여 가져오고 캐시합니다.표
eager_load
를 통합했기 때문에 표 요소를 통해 데이터를 줄일 수 있다.1 N 관련 표 LEFT JOIN의 SQL이 되돌아오는 기록이 중복되기 때문에 축소하기 어려우며, 중복을 방지하기 위해 디스틴act가 있는 조회를 발행하여 축소된 id 목록을 얻었다.그러나 이distinact의 SQL은 느린 검색이 되기 쉬워 1대 N 연결에 적합하지 않다.
includes
User.includes(:posts)
# User Load (3.1ms) SELECT `users`.* FROM `users` LIMIT 11
# Post Load (3.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
User.includes(:posts).where(posts: { id: 1 })
# SQL (1.9ms) SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1 LIMIT 11
# SQL (3.0ms) 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`.`content` AS t1_r1, `posts`.`user_id` AS t1_r2, `posts`.`created_at` AS t1_r3, `posts`.`updated_at` AS t1_r4 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1 AND `users`.`id` = 28
Post.includes(:user)
# Post Load (2.6ms) SELECT `posts`.* FROM `posts` LIMIT 11
# User Load (1.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (28, 22, 40, 5, 41, 1, 23, 11, 34, 47, 15)
Post.includes(:user).where(users: { name: 'user1' })
# SQL (2.6ms) SELECT `posts`.`id` AS t0_r0, `posts`.`content` AS t0_r1, `posts`.`user_id` AS t0_r2, `posts`.`created_at` AS t0_r3, `posts`.`updated_at` AS t0_r4, `users`.`id` AS t1_r0, `users`.`name` AS t1_r1, `users`.`created_at` AS t1_r2, `users`.`updated_at` AS t1_r3 FROM `posts` LEFT OUTER JOIN `users` ON `users`.`id` = `posts`.`user_id` WHERE `users`.`name` = 'user1' LIMIT 11
기본적으로preload
기능, where
등 축소 기능이 있을 때eager_load
와 같은 행동을 한다.여러 연관을 지정할 때 각 연관에 대해 다른 행동을 취하지 않고 모든 연관
preload
또는 eager_load
의 행동을 해야 한다.총결산
메서드
캐시
조회
참조 연관 대상 데이터
joins
하지 않다
INNER JOIN
할 수 있다
eager_load
하다
LEFT OURTER JOIN
할 수 없다
preload
하다
각자의 셀렉트.
할 수 있다
inclides
하다
상황에 따라
할 수 있다
기본적으로
include
조회를 통제하기 어려워서 잘 사용하지 않습니다. 1대1(belong to), N대1(has one)과 관련하여 LEFT JOIN으로 집중하는 것이 더 효율적이라고 생각합니다eager_load
,has one을 사용합니다.many(1대 N)의 경우 관련 사용preload
이 좋다.참고 문헌
Active Record Query Interface — Ruby on Rails Guides
Active Record Query Interface — Ruby on Rails Guides
ActiveRecord의 Joins, proeload, includes 및 eagerload의 차이점-Qita
Improving Database performance and overcoming common N+1 issues in Active Record using includes, preload, eager_load, pluck, select, exists? | Saeloun Blog
ORM에 대한 eager loading과 lazy loading은 매일 withnic의 웹 엔지니어입니다.
기사를 써주신 여러분께 감사드립니다.
나는 단지 손을 움직이면서 각양각색의 보도 정보를 총결하였을 뿐, 더욱 깊이 이해하고 추가, 수정하기를 바란다.
Reference
이 문제에 관하여(ActiveRecord의 N+1 질문 및 (proeload, eager load, includes, joins)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://qiita.com/craftcat/items/750feb2826ff96ac0b81텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)