n 개의 데이터
를 가져오면서 연관된 데이터를 가져오기 위해 n 번만큼
의 쿼리를 더 실행하면서 발생하는 문제이다.
명령을 실행할 때마다 데이터베이스에 접근하여 데이터를 가져오는 것이 아닌 실제 데이터를 사용해야할 때
쿼리문을 실행하는 방식이다.
예를 들어, 아래 코드를 보자.
실제 user의 데이터를 사용
하고 있다.user_list = User.objects.filter(is_active=True)
for user in user_list:
print(f"{user.name} 님의 계정은 활성화 상태입니다.") ---> 실제 쿼리 실행
djangoproject를 확인해보면, queryset이 평가되는 시점은 아래와 같다.
Iteration
(위의 예시)exists()
사용을 권장하고 있다.Slicing
User.objects.all()[:2]
또는 User.objects.all()[:25:5]
와 같이 슬라이싱하게 되면 쿼리가 실행되면서 평가가 이루어진다.Pickling/Caching
repr()
len()
count()
사용을 권장하고 있다. list()
list(User.objects.all())
과 같이 queryset을 list로 변경하게 되면 평가가 이루어진다.bool()
exists()
사용을 권장하고 있다.그렇다면 N+1 문제는 언제 발생
할까?
N+1 문제
가 발생하게 된다.machine_list = Machine.objects.filter(brand="APPLE")
for machine in machine_list:
print(f"{machine.user.name} 님이 보유하고 있는 기계의 id는 {machine.id} 입니다.")
django는 N+1 문제
를 해결하기 위해 2 개의 queryset 메서드를 제공한다.
아래의 두 가지 메서드는 기존 쿼리와 함께 관련 모델을 가져온다
는 점에서는 비슷하게 작동한다.
1:1 관계
, 1:N 관계
(N이 사용)INNER JOIN
SELECT
machine.id, machine.user_id, machine.name,
...
user.id, user.name,
...
FROM
machine
INNER JOIN user ON (machine.user_id = user.id)
WHERE brand = "APPLE"
N+1 문제
가 발생한 코드는 아래와 같이 수정할 수 있다. machine_list = Machine.objects.select_related("user").filter(brand="APPLE")
for machine in machine_list:
print(f"{machine.user.name} 님이 보유하고 있는 기계의 id는 {machine.id} 입니다.")
2N+1 문제
또한 해결 가능하다.Machine.objects.select_related("user", "user__country").filter(brand="APPLE")
iPhone15Pro 와 Macbook Air M1
이라면, 데이터는 아래와 같다.1:1 관계
, M:N 관계
, 1:N 관계
(1이 사용)user_list = User.objects.filter(is_active=True).prefetch_related("machine_set")
SELECT id, name, ...
FROM user
WHERE is_active = true
SELECT id, name, brand, ...
FROM machine
WHERE user_id IN (%s, %s, ...)
데이터를 캐싱
하기 때문에 더 이상의 쿼리는 실행되지 않는다.추가적으로 쿼리를 최적화할 수 있는 방법이다. 사용할 데이터만 추출해 사용하게 된다. 데이터베이스의 컬럼이 30개 인데 정작 사용하는 컬럼은 5개라면, 5개와 30개 중 몇개의 컬럼의 데이터를 추출해 사용하는 것이 효율적일까?
User.objects.annotate(c_name=F("country_name"))
.select_related("country").all()
.values("id", "name", "country__name")
# 결과
<Queryset [{"id": 1, "name": "nikevapormax", "c_name": "Republic of Korea"}, ...]>
User.objects.all().values_list("id", flat=True)
# 결과
<Queryset [1, 2, 3, 4, ...]>