본문 바로가기

Errors

[ruby on rails] n+1 문제 해결 경험

ruby on rails를 다루며 n+1 문제를 굉장히 많이 경험을 해보았고, 이 문제에 대한 해결은 디비에 요청을 최소화하기 위해 한 번 요청을 보낼 때, 연관관계에 있는 필요한 데이터를 한 번에 가져오는 것이다. 이때, 두 가지 방법으로 문제를 해결했다.

 

1. 첫 번쨰 방법 : preload() or includes()

두 함수의 장단점 약간의 차이점은 있지만, 날아가는 쿼리는 동일하다. 연관관계에 있는 모든 값들을 미리 가져온다. 그래서 한 모델에서 연관관계에 있는 모델에 직접 접근할 때 사용하면 굉장히 편하게 쓸 수 있다.

 

1) 언제 사용?

한 모델에 연관관계에 있는 모델에 적접 접근할 때 사용하면 굉장히 편하게 사용할 수 있다.

 

2) 예시

store = '도매'
wholesale_stores = WholesaleStore.includes(store_number: [building_floor: :building]).where('store LIKE ?', "%#{store}%")
# wholesale_stores = WholesaleStore.preload(store_number: [building_floor: :building]).where('store LIKE ?', "%#{store}%")

이렇게 호출하면 쿼리를

WholesaleStore Load (44.5ms)  SELECT `wholesaleStore`.* FROM `wholesaleStore` WHERE (store LIKE '%도매%')
  StoreNumber Load (30.6ms)  SELECT `storeNumber`.* FROM `storeNumber` WHERE `storeNumber`.`id` IN (25546, 18017136, 16221, 18172)
  BuildingFloor Load (47.2ms)  SELECT `buildingFloor`.* FROM `buildingFloor` WHERE `buildingFloor`.`id` IN (1, 3, 4, 8)
  Building Load (64.9ms)  SELECT `building`.* FROM `building` WHERE `building`.`id` IN (1, 2, 5, 6)

호출한 연관관계의 크기만큼 쿼리를 날려준다. 다수의 도매가 존재할 시, 위에 있는 id값들마다 매번 쿼리를 날려줘야 해서 속도가 엄청 느려질 수 있다. 그래서 이렇게 includes나 preload를 사용하면 한 번에 데이터를 로드해줘서 n+1문제를 방지할 수 있다.

 

3) 사용 예시

wholesale_stores.each do |ws|
  sn = ws.store_number
  bf = ws.store_number.building_floor
  b = ws.store_number.building_floor.building
end

위에서 includes 나 preload로 미리 로드를 하게 되면 이렇게 연관관계에 접근했을 때, 쿼리를 날리지 않고 데이터를 가져올 수 있다. 이렇게 연관관계를 사용해 데이터를 가져올 때 includes를 사용해주면 좋다.

 

2. 두 번째 방법 : joins()

첫 번째 방법은 연관관계 개수만큼 쿼리가 날라간다. 하지만 joins로 데이터를 가져오면 inner join으로 한 번의 쿼리로 데이터를 가져온다.

 

1) 언제 사용?

데이터를 한 번에 가져와서 하나의 모델로 바라보는 것이 아니라 하나의 객체로 바라보고 데이터를 처리할 때, 사용하면 좋다.

이렇게 처리해주는게 가독성도 좋고, 속도도 더 빠른 경우라고 판단이 들면, 이러한 방식을 사용한다.

나는 한 모델에서 연관관계에 직접 접근이 필요한 경우(includes())가 아니라면, 항상 joins로 데이터를 가져왔다.

이때 물론 null값 포함되는 경우에는 left outer joins로 가져오는 경우도 존재한다.

 

2) 예시

@sheets = SheetGood.get_data(ids)

class SheetGood
  scope :get_data, -> (ids) {
    joins(:order_sheet)
      .joins(sheet_goods_infos: [store_goods_infos: [sheet_store: [w_sheet: :w_sheet_group]]])
      .left_joins(s_sheet: :stop_order)
      .where("wSheetGroup.status != 'Deleted'")
      .where(id: ids)
      .select('sSheet.id as sId,
            sSheet.od,
            sSheet.ie,
            sSheet.wsId,
            sSheet.gId,
            sSheet.rsId,
            sSheet.type,
            sheetGoods.gn,
            sheetGoodsInfo.col,
            sheetGoodsInfo.size,
            sheetGoodsInfo.price,
            sheetGoodsInfo.count,
            stop_orders.status AS stopOS')
  }
end

이렇게 조인을 사용해서 데이터를 가져올 때 select를 꼭 써줘서 가져와야 한다. 그렇지 않으면, join을 호출한 최초 모델의 칼럼들만 조회할 수 있기 때문이다. 그리고 테이블 간 칼럼들의 이름이 중복되는 경우 AS를 사용해서 이름을 바꾸어줘야 모호한 에러가 나지 않는다.

 

3) 사용 예시

보통은 저렇게 데이터를 가지고 오면 json으로 넘겨줘서 프론트에서 처리하기 편하게 해주는 경우가 많다. 항상 예외는 존재하므로, controller나 service 계층에서 직접 사용한다면, 

@sheets.each do |s|
  s.type
end

이러한 방식으로 접근해서 데이터를 사용할 수 있다.

'Errors' 카테고리의 다른 글

[ruby on rails] n+1 해결한 기억나는 경험  (0) 2021.12.24
에러를 해결하기 위한 마음가짐  (0) 2021.12.17
기억나는 에러들  (0) 2021.11.05