- 오라클
- Oracle
- Django
- 코딩테스트 python
- 코딩테스트
- 빅데이터분석기사
- SQL
- 파이썬
- 기초다지기
- 이것이 취업을 위한 코딩테스트다 with 파이썬
- DB
- 부스트코스
- PY4E
- 빅분기
- r
- 네이버부스트캠프
- [멀티잇]데이터 시각화&분석 취업캠프(Python)
- 이기적
- Ai
- 데이터베이스
- boostcourse
- 빅데이터 분석 기반 에너지 운영 관리자 양성 및 취업과정
- python
- unsub
- 정보처리기사
- 프로그래머스
- AI 플랫폼을 활용한 데이터 분석
- 인공지능기초다지기
- boostcoures
- 데이터 분석 기반 에너지 운영 관리자 양성 및 취업과정
- Today
- Total
매일공부
[Django] select_related 하나로 쿼리가 490개→29개 준 이유 본문
Q. brand만 추가했는데 어떻게 쿼리 최적화가 된 거야?
.select_related("brand")는 Django ORM에게 "이 쿼리 하나로 관련 테이블까지 SQL JOIN해서 한 번에 가져와라"고 지시하는 것이다.
기존 코드가 왜 느렸나 — N+1 문제
qs = Product.objects.filter(is_deleted=False).order_by("prod_id") # 쿼리 1개
이건 product 테이블만 조회한다. brand 컬럼은 FK(외래키) ID값만 가져오고, 실제 브랜드 객체는 아직 안 가져온 상태다 — Django의 FK는 기본적으로 지연 로딩(lazy loading)이라, product.brand를 실제로 "접근"하는 순간에야 별도 쿼리를 날린다.
그런데 Product.__str__()(템플릿에서 제품을 출력할 때 자동 호출됨)이 이런 식으로 되어 있었다.
def __str__(self):
return f"[{self.prod_id}] {self.brand.brand_name} ..." # self.brand 접근!
즉 제품 200개를 순회하면서 한 줄씩 출력할 때마다 product.brand에 접근 → 캐시된 게 없으니 매번 SELECT * FROM sw_brand WHERE id = 'xxx'가 개별로 날아간다. 제품 조회(1번) + 제품 200개 × 브랜드 조회(200번) = 201번 쿼리. 이게 N+1 문제다 — 조회 1번(1) + 그 결과 각각에 대해 추가 조회 N번.
select_related("brand")가 하는 일
qs = Product.objects.filter(is_deleted=False).select_related("brand").order_by("prod_id")
이러면 Django가 SQL을 이렇게 바꾼다.
SELECT product.*, sw_brand.*
FROM product
INNER JOIN sw_brand ON product.brand_id = sw_brand.id
WHERE product.is_deleted = 0
한 번의 JOIN 쿼리로 제품과 브랜드 데이터를 동시에 가져와서, Django가 각 Product 객체 안에 brand 객체를 미리 캐싱해둔다. 그러면 나중에 self.brand.brand_name을 호출해도 이미 메모리에 있는 값을 쓰니까 추가 쿼리가 안 나간다.
.select_related("brand") 하나 추가한 게 "필드 하나 더 가져온 것"처럼 작아 보이지만, 실제로는 제품 개수만큼 반복되던 쿼리를 JOIN 1번으로 합친 것이라 영향이 컸다. 이게 폼의 드롭다운(제품 전체 목록)과 목록 화면(각 행마다 제품 이름 출력) 양쪽에 다 적용되면서, 490개였던 쿼리 수가 29개로 크게 줄었다.
Q. 처음엔 왜 그렇게 짰던 거야? 그것도 장점이 있어서 그런 거 아닌가?
딱히 의도된 트레이드오프는 아니었다. Django ORM의 기본 동작 자체가 지연 로딩이라, 자연스럽게 이런 구조로 짜이기 쉽다. 몇 가지 이유가 있다.
1. Django FK는 기본이 지연 로딩 — "안 쓰면 안 가져온다"가 원칙
Product.objects.filter(...)만 써도 코드는 정상 동작한다. .brand를 실제로 접근하기 전까진 쿼리가 안 나가니, 작성 시점엔 "일단 필요한 것만 조회한다"는 게 더 자연스러운 기본값이다. select_related는 "나중에 관련 필드도 쓸 거다"를 미리 알려주는 최적화 힌트라서, 이걸 안 붙인다고 코드가 틀린 게 아니라 그냥 "최적화를 안 한 상태"일 뿐이다.
2. __str__()이 관련 필드를 쓴다는 게 호출부에서는 안 보임
폼의 쿼리셋을 짤 때는 "제품 목록 쿼리셋"만 생각하지, 그게 템플릿에서 출력되면서 내부적으로 __str__() → self.brand.brand_name까지 타는 건 한눈에 안 보인다. 이게 N+1의 전형적인 발생 패턴이다 — 원인(brand 접근)과 결과(느린 페이지)가 코드상 멀리 떨어져 있어서, 처음 짤 때는 놓치기 쉽다.
3. 데이터가 적을 땐 체감이 안 됨
개발 초기에 데이터가 몇 개 안 될 때는 200번 vs 1번 쿼리 차이가 몇 밀리초 차이로 안 느껴진다. 데이터가 쌓이고 나서야 체감될 정도로 느려지는 게 N+1의 특징이라, 처음 짤 때 이 문제를 예측하기가 쉽지 않다.
Q. 그럼 select_related 안 쓰는 게 나은 경우도 있나?
있다.
- 관련 필드를 아예 안 쓸 때는 JOIN이 오히려 불필요한 컬럼까지 끌고 오는 오버헤드가 된다
- 1:N(정방향 FK가 아니라 역방향) 관계처럼 결과가 여러 개인 경우엔 select_related가 아니라 prefetch_related를 써야 한다 — JOIN으로 묶으면 행이 뻥튀기되기 때문이다
이번 케이스는 FK 하나(제품→브랜드, N:1 방향)를 무조건 쓰는 상황이라 select_related가 명백히 맞는 선택이었고, 원래 코드는 그냥 이 최적화를 빠뜨린 것에 가까웠다.
정리
상황 선택
| FK로 연결된 단일 객체를 항상 쓴다 (N:1, 1:1) | select_related — JOIN으로 한 번에 |
| 역방향 FK/M:N으로 연결된 여러 객체를 쓴다 (1:N) | prefetch_related — 별도 쿼리 후 Python에서 매칭 |
| 관련 필드를 아예 안 쓴다 | 아무것도 안 씀 — 괜히 JOIN하면 오버헤드 |
'IT > 작업기록' 카테고리의 다른 글
| [Django] 화면에서 로고 이미지가 깨지는데 PDF에는 정상 — DEBUG=False의 함정 (0) | 2026.08.27 |
|---|---|
| [배포] PDF는 한글이 정상인데 운영서버 배포 후에만 깨진 이유 (0) | 2026.08.27 |
| 시놀로지 NAS + 라우터로 내부 서비스에 서브도메인 연결하기 (IP:포트 대신 도메인으로 접속하기) (0) | 2026.08.20 |
| [DB] 개발 DB 민감정보 마스킹하기 — 어디까지, 어떻게 (0) | 2026.08.19 |
| [Django+WeasyPrint] PDF 템플릿만 CSS가 인라인인 이유 (0) | 2026.08.18 |