N+1 문제
references:
https://zetawiki.com/wiki/N%2B1_%EC%BF%BC%EB%A6%AC_%EB%AC%B8%EC%A0%9C
https://velog.io/@kim6515516/npuls
N+1 쿼리 문제
N+1는 DB 쿼리를 사용할 때에 피해야 할 안티패턴의 예시 중 하나이다.
쿼리 1번으로 N건을 가져 왔는데, 그것과 관련한 추가 내용을 얻기 위해 쿼리를 추가로 수행하는 상황을 말한다.
이게 문제가 되는 이유는, 여러번의 쿼리를 보내게 되면 성능적인 이슈가 발생 할 수 있기 때문이다.
주로 사람이 직접 쿼리를 짜는 경우에는 해당 문제를 의식하고 있는 환경이기 때문에 자연스레 회피하게 되지만, 그렇지 않은 경우(ORM) 에 발생하는 문제이다.
예시 (Django)
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
def __str__(self):
return self.name
class Restaurant(models.Model):
place = models.OneToOneField(Place, on_delete=models.CASCADE, related_name='restaurant')
name = models.CharField(max_length=50)
severs_pizza = models.BooleanField(default=False)
def __str__(self):
return self.name
위의 코드를 보면 알겠지만, Restaurant 모델과 Place 모델이 서로에게 restaurant 와 place라는 이름으로 연결 되는것을 알 수 있다.
다음의 코드를 보자.
for place in Place.objects.all():
print(place.restaurant.name)
Place의 모든 레코드를 가져와서, 그 안에있는 restaurant 들의 모든 내용을 갖고오는 내용이다.
그런데 Place 모델의 레코드를 로드 하더라도, 연결되어있는 추가 필드(지금의 상황에서는 Restaurant 필드)는 로드되지 않는다.
그렇다면, 저 코드의 경우 Place의 모든 레코드를 갖고오는 쿼리 한번, 그리고 place의 갯수만큼 쿼리를 실행해야 된다.
실제로 코드로 보면 다음과 같을것이다.
모든 place 레코드 가져오기
SELECT `photo_place`.`id`, `photo_place`.`name`, `photo_place`.`address` FROM `photo_place`;
각각의 레코드를 하나하나 가져오기
SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` = 1 LIMIT 21; SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` = 2 LIMIT 21; SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` = 3 LIMIT 21; ...
만약 place 레코드가 십만개라면, 쿼리도 십만개가 실행이 될 것이다.
이러한 상황이 N+1 문제이다.
해결방안
그래서, eager 로딩 이라는 방법을 사용한다.
django에서는 이를 다음과 같이 구현한다.
for place in Place.objects.prefetch_related('restaurant').all():
print(place.restaurant.name)
prefetch_related 라는 메소드는, 인자값으로 받은 related(OneToOne 처럼 연결된 필드) 필드를 미리(pre) 로드(fetch) 하는 메소드이다.
place 레코드가 6개라고 치고, 실제 실행되는 쿼리는 다음과 같을것이다.
SELECT `photo_place`.`id`, `photo_place`.`name`, `photo_place`.`address` FROM `photo_place`;
SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` IN (1, 2, 3, 4, 5, 6);
아까 where 절로 해당하는 place_id를 하나하나 가져온것과 달리, IN 구문을 통해서 place_id에 해당 될 내용을 한번에 적어 값을 받아오는것을 볼 수 있다.
Comments
Post a Comment