https://scoutapm.com/blog/django-and-the-n1-queries-problem
코드로 보면
visitors = Visitor.objects.filter(visit_date__year=2022)
for visitor in visitors:
print(f"{visitor.person.name}. visited on {visitor.visit_date}")
# person은 visitor에 연결된 FK object다.
person
을 가져오지 않는다. person
에 대한 정보를 가져오기 위해 해당 line에서 Person
객체에 대한 query를 실행한다. 이처럼 N개의 visitors를 순회하면서 person의 name을 얻기위해 django는 N개만큼(visitors의 수)의 person query를 더 수행하게 된다.
database에서 확인하면,
SELECT id, person_id, visit_date, ...
FROM visitors
WHERE year(visit_date) = 2022
SELECT id, name, phone_number, ...
FROM person
WHERE id = %s
이런식으로 불러와야할 visitor의 갯수가 많아질 수록 추가적으로 불러올 query의 실행 횟수가 많아지게 된다.
이러한 문제는 데이터베이스의 구조가 복잡해질수록 성능에 영향을 끼친다.
이를 위해서는 django의 lazy한 특성을 예방해주는 방법이 있다.
먼저 django의 lazy-loading
에 대해 알고있으면 좋다.
lazy-loading
이란 django에서 ORM을 작성할때, queryset에 담겨있는 데이터를 이용할 때에 SQL문을 호출하는 것이다.
django는 이러한 성능 문제를 해결할 수 있도록 두가지 방법을 제공한다.
select_related()
와 prefetch_related()
다.
이는 lazy-loading을 피하기 위해 사전에 사용할 data를 가져오는 method
이다. 이를 eager-loading
방식 이라고 한다.
두 method
의 차이점은 select_related()
는 같은 쿼리내에서 관련된 instances
를 가져오는 것이고, prefetch_related()
는 두번째 쿼리에서 가져온다는 것이다.
select_related()
위에서 다룬 N+1 문제가 발생하는 query에 select_related("person")
를 추가해보았다.
visitors = Visitor.objects.filter(visit_date__year=2022).select_related("person")
for visitor in visitors:
print(f"{visitor.person.name}. visited on {visitor.visit_date}")
이제 for loop를 통해 visitors 에 대한 query를 실행하고 각각에 대해 접근할 때 더 이상 N+1 문제는 발생하지 않는다.
database에서 확인하면
SELECT
visitors.id, visitors.person_id, visitors.visit_date,
...
person.id, person.name,
...
FROM
visitors
INNER JOIN persons ON (visitors.person_id = person.id)
WHERE year(visit_date) = 2022
각 query의 결과에 visitors table과 person table 정보가 INNER JOIN
을 통해 같이 있는것을 확인할 수 있다.
prefetch_related()
위에서 다룬 N+1 문제가 발생하는 query에 prefetch_related("person")
를 추가해보았다.
visitors = Visitor.objects.filter(visit_date__year=2022).prefetch_related("person")
for visitor in visitors:
print(f"{visitor.person.name}. visited on {visitor.visit_date}")
prefetch_related()
는 select_related()
와 달리 INNER JOIN
이 아닌 또 하나의 query를 실행한다. 첫번째는 visitors
테이블에 대한 query, 두번째는 person
테이블에 대한 query이다.
prefetch는 아래와 같이 query를 실행한 후 django의 "joins"를 이용하여 메모리상에서 visitors와 관련된 person을 연결한다.
먼저 visitors에 대한 query를 실행한다.
SELECT id, person_id, visit_date, ...
FROM visitors
WHERE year(visit_date) = 2022
그 다음 visitors에 대한 쿼리 결과를 통해 person에 대한 query를 실행한다.
SELECT id, name, phone_number, ...
FROM person
WHERE id IN (%s, %s, ...)
이제 print를 통해 person의 name에 접근할 때, 매 loop마다 django는 person에 대한 추가적인 query를 실행하지 않는다. memory상에 django joins
를 통해 관련 정보가 연결되어 있기 때문이다.
prefetch_related()
가 갖는 이점prefetch_related()
는 중복된 query를 실행하지 않는다.select_related()
의 경우 INNER_JOIN
을 사용하기 때문에, 해당 방문객에 대한 고객 정보를 확인할 때 2022년에 방문한 방문객
을 조회할 때 A
라는 방문객(visitor)이 여러번 방문할 경우 해당 고객(person)에 대한 query를 다시 실행하게 된다.M2M
, ManyToOne
관계에 사용할 수 있다.두 method
중 무엇을 사용해야할 지 모를 경우에는 prefetch_related()
를 사용하자. 대부분의 경우에는 prefetch_related()
가 더 효율적이라고 한다.