99클럽 TIL
99클럽 코테 스터디 15일차 TIL DB N+1 문제
차가리
2024. 6. 3. 20:31
728x90
- DB
- Index
Index 작동 원리- Index 종류
B-Tree 인덱스Hash IndexesBitmap IndexGIST(Generalized Search Tree)R-TreeFull Text IndexSpatial IndexTrie(Prefix Tree) IndexCovering Index
- Index
영속성트랜잭션- ORM
- ACID
N+1 문제- DB 정규화
- Data Replication
- sharding 전략
- CAP 이론
란?
- DB 쿼리의 성능 최적화와 관련된 일반적인 문제.
- 특히, ORM 프레임워크를 사용할 때 자주 발생함. 주로 데이터베이스에 불필요하게 많은 쿼리를 발생시키기 때문에 성능 저하를 유발함.
원인
- N 개의 부모 엔티티와 각각의 부모 엔티티에 관련된 자식 엔티티들
- 예시상황
- 예를 들어, 블로그 시스템에서 여러 블로그 글(부모 엔티티)을 가져오고, 각 글마다 여러 댓글(자식 엔티티)을 가져오는 상황.
- 문제 근원
- 초기 쿼리 : 모든 부모 엔티티를 가져오는 단일 쿼리가 실행됨. 예를 들어,
SELECT * FROM posts
- 추가 쿼리 : 각 부모 엔티티에 대해 관련된 자식 엔티티를 가져오기 위해 추가 쿼리가 실행됨. 예를 들어, 각
post
에 대해SELECT * FROM comments WHERE post_id = ?
이 때, 만약 10개의 포스트를 가져오면, 총 11개의 쿼리가 실행됨.(1개의 부모 쿼리 + 10개의 자식 쿼리).
이 문제가 발생하는 이유는 ORM이 각 부모 엔티티에 대해 자식 엔티티를 개별적으로 가져오기 때문임.
- 초기 쿼리 : 모든 부모 엔티티를 가져오는 단일 쿼리가 실행됨. 예를 들어,
- 예시상황
발생 후 대처법
확인 방법
- 로그 분석
- DB log 나 ORM 쿼리 로그를 분석하여 동일한 유형의 쿼리가 반복적으로 실행되는지 확인함.
- 프로파일링 도구 사용
- SQLAlchemy, Django ORM 등에서 제공하는 프로파일링 도구를 사용하여 쿼리 실행 패턴을 분석함.
대처
- 모니터링
- 응답 시간이 긴 쿼리나 성능 병목을 일으키는 부분을 모니터링함.
- 캐싱 사용
- 반복적으로 동일한 데이터를 조회하는 경우, 캐싱을 통해 데이터베이스 접근을 줄일 수 있음.
해결방법
- Eager Loading
- 처음부터 필요한 모든 데이터를 한꺼번에 가져오는 방법
- SQLAlchemy
joinedload
또는subqueryload
를 사용하여 관련 데이터를 미리 로드할 수 있음.
from sqlalchemy.orm import joinedload
posts = session.query(Post).options(joinedload(Post.comments)).all()
- Django ORM
select_relate
또는prefetch_related
를 사용하여 관련 데이터를 미리 로드할 수 있음.
posts = Post.objects.select_related('author').all() # ForeignKey 관계
posts = Post.objects.prefetch_related('comments').all() # Many-to-Many 관계
- Batch Processing
- 한 번에 여러 개의 엔티티를 처리하도록 쿼리를 조정하는 방법. 이를 통해 반복되는 쿼리 호출을 줄일 수 있음.
- In-Clause 사용
- 여러 개의 ID를 한 번에 가져오는 쿼리를 작성하여 필요한 데이터를 한꺼번에 가져옴.
SELECT * FROM comments WHERE post_id IN (1,2,3,4,5);
- 데이터 모델링 최적화
- 데이터 모델링을 최적화하여 불필요한 쿼리 발생을 줄임. 예를 들어, 데이터 정규화나 역정규화를 통해 자주 사용하는 데이터를 함께 저장함.
- 캐싱 도입
- 자주 조회되는 데이터는 캐시에 저장하여 데이터베이스 접근을 최소화함.
- Redis, Memcached 같은 캐시 서버를 사용할 수 있음.
- Django 캐시 프레임워크
from django.core.cache import cache
posts = cache.get('all_posts')
if not posts:
posts = Post.objects.all()
cache.set('all_post', posts)
728x90