
쿼리 N+1 문제
- 쿼리 한번으로 N건의 데이터를 가져왔을때, 원하는 데이터를 얻기위해 N건의 데이터를 가져온 데이터 수만큼 반복해서 2차적으로 쿼리를 수행하는 성능 이슈
해결 방법 : Eager-loading
- lazy-loading를 피해야 한다.
- lazy-loading : 데이터에 접근하여 필요할 때마다 데이터를 가져오는 방식을 의미합니다. 초기에는 메모리를 효율적으로 사용할 수 있습니다.
- Eager-loading : lazy-loading과 반대 개념으로, 데이터를 미리 가져와서 필요한 경우에 대비하는 방식을 의미합니다.
- django는 Eager-loading방식으로 바꾸는 두가지 방법을 제공함.
- select_related() 와 prefetch_related()다. 두 메서드 모두 ORM(객체 관계 매핑)을 사용할 때 성능 최적화를 위해 사용되는 method이다.
- 두 method의 차이점은 select_related()는 같은 쿼리내에서 관련된 instances를 가져오는 것이고, prefetch_related()는 두번째 쿼리에서 가져온다는 것임.
- 'select_related()'는 ForeignKey나 OneToOneField와 같은 정방향 참조 관계를 가진 모델을 미리 가져와서 쿼리의 JOIN을 통해 데이터베이스에서 한 번에 가져오는 메서드. 이는 N+1 문제를 방지하고, 성능을 향상시킬 수 있다.
- 주로 정방향 참조를 통해 연결된 객체의 필드를 사용할 때 효과적이며 적은 수의 쿼리로 관련된 객체를 가져올 수 있기 때문에, 단일 객체에 대한 성능 최적화에 유용하다.
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
from django.db import connection
books = Book.objects.select_related('author').all()
for book in books:
print(book.title)
print(book.author.name)
print("쿼리 갯수:", len(connection.queries))
> Book1
Author1
Book2
Author2
...
쿼리 갯수: 1
- 'prefetch_related()'는 ForeignKey, OneToOneField 뿐만 아니라 ManyToManyField 등 모든 종류의 관계에 사용할 수 있습니다.
- 정방향 참조와 달리, 역참조에서 필요한 데이터를 미리 조회하여 쿼리 결과를 가져온다.
- 이는 별도의 쿼리를 실행하여 필요한 데이터를 가져오므로, 한 번에 여러 관계를 처리할 때 유용하다.
- 단점으로는 추가적인 쿼리를 실행하므로, 일부 상황에서는 select_related보다 더 많은 쿼리를 발생시킬 수 있다.
class Genre(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
genres = models.ManyToManyField(Genre)
from django.db import connection
books = Book.objects.prefetch_related('genres').all()
for book in books:
print(book.title)
for genre in book.genres.all():
print(genre.name)
print("쿼리 갯수:", len(connection.queries))
> Book1
Genre1
Genre2
Book2
Genre3
Genre4
...
쿼리 갯수: 2
- prefetch_related은 원래의 main query가 실행된 후 별도의 query를 따로 실행하고 select_related은 하나의 query만으로 related objects들을 다 가져옴.
- ManyToMany, ManyToOne의 관계에서는 prefetch_related를 사용해야 하지만 foreign_key, OneToOne과 같은 single-valued 관계가 있는 곳에서는 최대한 select_related를 사용하여 query수를 줄여주는 것이 좋을 수 있다.
- 어떤 경우에는 둘을 조합하여 최적의 성능을얻을 수 있음.
class Author(models.Model):
name = models.CharField(max_length=100)
class Genre(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
genres = models.ManyToManyField(Genre)
from django.db import connection
books = Book.objects.select_related('author').prefetch_related('genres').all()
for book in books:
print(book.title)
print(book.author.name)
for genre in book.genres.all():
print(genre.name)
print()
print("쿼리 갯수:", len(connection.queries))
> Book1
Author1
Genre1
Genre2
Book2
Author2
Genre3
Genre4
...
쿼리 갯수: 2