N+1 Problem

fana·2022년 4월 1일
0
post-thumbnail

서론

N+1 Query Problem에 대한 이야기는 많지만 작은 서비스나 프로젝트를 하다보면 간과하고 지나치는 경우가 많다.
최근 서비스의 데이터 규모가 커지면서 이전에 간과했었던 곳곳에서 성능이 크게 저하되는 현상이 생겼는데 모두 쿼리 최적화 관련된 문제였다.

문제

대규모 데이터가 필요한 작업이라거나 결과물이 단순 CSV, 엑셀파일로도 가능하다면 SQL을 그냥 작성하니까 이런 문제가 없겠지만 ORM으로 대규모 데이터를 다루게 되면 N+1 쿼리 문제를 항상 생각해야 한다. 흔하게 겪는 문제는 다음과 같다.

# Query 1
users = User.joins(:favorites)
			.where("users.status = ?", status)
		    .where("favorites.type = ?", type)
		    .where("favorites.active = ?", true)
         
# Query N
users.each do |user|
  if user.favorites.last.name == "something"
    do_something()
  end
end

첫번째 쿼리로 users가 N명이라면, 위 코드는 쿼리가 총 N+1번 발생한다.
테이블 조인이 많아지거나, users가 많아지거나, do_something 매서드에서 다른 테이블을 더 많이 참조한다면 성능은 더 떨어지게 된다.
이런 고전적인 N+1 쿼리 최적화문제를 해결하는 방법도 ORM 자체에서 제공해준다. 상황에 맞게 잘 사용하면 되는데 보통은 다음과 같다.

해결방법

1. SQL 작성

자바나 코틀린처럼 @Query 어노테이션으로 SQL을 직접 작성할 수 있을수도 있다.

// kotlin
@Repository
interface UserRepository : R2dbcRepository<User, Int> {
  @Query("""
    select 
      u.id,
      u.name,
      f.name,
      f.type
    from users u
    left join favorites f
    on u.id = f.user_id
    where 
      u.status = 1 and f.type = 'type' and f.active = 1
  """)
  fun findSomeUserInfos(): Flux<UserInfo>
}

2. Eager Loading

여기서는 ruby와 ruby on rails의 ORM인 ActiveRecord 기준으로 작성해보았다.

# Query 1
users = User.joins(:favorites)
			.where("users.status = ?", status)
		    .where("favorites.type = ?", type)
		    .where("favorites.active = ?", true)
            .includes(:favorites)
            .references(:favorites)

# Eager Loading된 상태
users.each do |user|
  if user.favorites.last.name == "something"
    do_something()
  end
end

위 예시코드는 쿼리가 한번말 발생한다. 첫 쿼리로 favorites 에 대한 정보를 모두 불러오기 때문이다.

3. 필드 직접 선택하기

이걸 뭐라고 표현해야할지 모르겠는데 예제 코드로 바로 설명하면 다음과 같다.
ActiveRecord.pluck 으로 직접 필드를 선택한 다음 할당한다.

user_id, user_name, favorite_name, favorite_type 
		= User.joins(:favorites)
				.where("users.status = ?", status)
                .where("favorites.type = ?", type)
                .where("favorites.active = ?", true)
				.pluck("users.id, users.name, favorites.name, favorites.type")
# do something

결론

N+1 쿼리 문제와 관련된 최적화는 사실 해결방법이 그렇게 어렵다고 느껴지지는 않는다.
다만 우리가 ORM 등을 사용하면서 그런 문제에 대해 생각하지 않고 쉽게 지나치게 되는 것들이
점점 습관이 되면서 한번에 성능 문제를 떠안게 된다.

한번 제대로 겪고나면 이 성능문제를 고려하는 것 자체는 어려운 일이 아니니 습관을 잘 들여야겠다.

0개의 댓글