
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()가 더 효율적이라고 한다.