이번에는 N + 1 쿼리 문제에 대해서 알아보고자 한다.
N + 1 쿼리 문제
쿼리 한번으로 N건의 데이터를 가져 왔을 때, 원하는 데이터를 얻기 위해 N건의 데이터를 가져온 데이터 수만큼 반복해서 2차적으로 쿼리를 수행하는 문제를 의미한다.
이해를 돕기 위해서 Django의 코드를 통해 설명하고자 한다.
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다.
1. 먼저 Visitor model을 통해 오늘 방문한 방문객들의 Queryset을 생성한다. (Django의 lazy한 특성 때문에, 해당 쿼리는 아직 실행되지 않는다.)
2. 그 다음 선언한 visitors를 for loop을 통해 순회한다.
3. 각 visitor를 순회하며 코드를 실행한다. 이때 django는 visitor에 할당된 foreign key person을 가져오지 않는다. person에 대한 정보를 가져오기 위해 해당 line에서 Person객체에 대한 query를 실행한다.
DB에서 확인을 해보면,
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한 특성 : ORM에서 명령을 실행할 때마다 데이터베이스에 접근하여 데이터를 가져오는 것이 아닌 모든 명령처리가 끝나고 실제 데이터를 불러와야 할 때, 데이터베이스 query문을 실행하는 방식을 말한다.
해결방안
django에서는 이러한 성능 문제를 해결할 수 있도록 select_related()와 prefetch_related() 라는 2가지 방법을 제공한다. 이는 lazy-loding을 피하기 위해 사전에 사용할 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")을 추가한다.
isitors = 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 테이블에 대한 qurey, 두번째는 person 테이블에 대한 query이다. prefetch는 아래와 같이 query을 실행한후 django의 joins을 이용하여 메모리상에서 visitors와 관련된 person을 연결한다.
SELECT id, person_id, visit_date, ...
FROM visitors
WHERE year(visit_date) = 2022
SELECT id, name, phone_number, ...
FROM person
WHERE id IN (%s, %s, ...)
이제 매 loop마다 django는 person에 대한 추가적인 query을 진행하지 않는다. 메모리상에 django joins을 통해 관련 정보가 연결되어 있기 때문이다.
'Computer Theory > Web' 카테고리의 다른 글
HTTP 메서드 종류 (1) | 2024.05.03 |
---|---|
API에 대하여 (0) | 2023.12.12 |
Transaction과 ACID (0) | 2023.12.09 |
ORM(Object Ralational Mapping) (0) | 2023.12.09 |
RDB와 NOSQL DB (0) | 2023.12.09 |