ActiveRecord의 N+1 질문 및 (proeload, eager load, includes, joins)

11335 단어 RubyRails

개시하다


액티브 레코드에서 여러 테이블을 넘나드는 검색을 할 때 N+1 문제를 해결하기 위해 사용된 기사includes를 자주 보고 이를 참고했지만, 그다지 좋지 않다고 해 각종 조사 결과를 정리한 메모다.

문제에 관하여


순환 처리 중 매번 SQL을 발행하고 대량의 SQL을 발행하여 성능이 떨어지는 문제를 가리킨다.
ActiveRecord와 같은 OR Mapper를 사용할 경우 발생하기 쉽습니다.

왜 일어났을까


읽기 지연이 가능한 ActiveRecord의 기본값입니다.관련 양식이 필요할 때, 검색어를 발행하고 필요한 값을 꺼냅니다.
메모리를 확보하는 양은 적지만 관련 표를 사용할 때마다 SQL이 발매되어 동작이 무거워진다.
예를 들어 어떤 일람표를 만들 때
  • 목록에 표시된 데이터 한 번 실행 ->SELECT(N개 기록 획득)
  • N회 기록에 관한 데이터 획득 ->SELECT
  • 이 동작은 총 N+1 조회를 수행하며 N이 많을수록 성능이 떨어집니다.

    해결책


    대량 읽기라고도 부른다.
    사전에 관련 표를 모두 메모리에 저장했음을 나타낸다.
    조회가 적어도 되기 때문에 묘사가 빨라지지만 관련 표도 모두 메모리에 있기 때문에 해당하는 메모리를 소모한다.
    건수를 줄여서 사용하는 것이 좋다.
    사용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,orderjoins를 추천합니다.

    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의 웹 엔지니어입니다.
    기사를 써주신 여러분께 감사드립니다.
    나는 단지 손을 움직이면서 각양각색의 보도 정보를 총결하였을 뿐, 더욱 깊이 이해하고 추가, 수정하기를 바란다.

    좋은 웹페이지 즐겨찾기