🗄️
Django ORM 내부 — QuerySet이 lazy한 이유
filter().exclude().order_by()를 체이닝해도 SQL이 안 나가는 원리
# 이 시점에서 SQL은 실행되지 않는다
qs = User.objects.filter(is_active=True).exclude(role='admin').order_by('-created_at')
# 여기서 비로소 SQL 실행
users = list(qs)
QuerySet은 SQL 쿼리를 "설명하는 객체"이지, 실행 결과가 아니다.
내부 구조
QuerySet 내부에는 query 속성이 있다. 이건 django.db.models.sql.Query 객체로, WHERE 조건, ORDER BY, JOIN 정보를 트리 구조로 보관한다.
filter()를 호출하면 새 QuerySet을 복제(_clone())하고, query.add_q()로 Q 객체를 조건 트리에 추가한다. SQL 문자열은 아직 생성되지 않는다.
SQL이 생성되는 시점은 query.get_compiler().as_sql()이 호출될 때다. 이건 QuerySet이 "평가"될 때 — __iter__, __len__, __getitem__, list() 등.
왜 lazy인가
- 체이닝 최적화 — 여러 조건을 쌓아놓고 한 번만 SQL 실행
- 재사용 — 같은 QuerySet에 다른 조건을 추가해서 여러 쿼리를 만들 수 있다
- 불필요한 쿼리 방지 — 평가 안 하면 SQL이 안 나간다
N+1 문제가 여기서 나온다
for user in User.objects.all(): # 1번 쿼리
print(user.profile.bio) # N번 쿼리 (매번 새 SQL)
select_related(JOIN) 또는 prefetch_related(별도 쿼리 + Python에서 조합)로 해결한다.
핵심 포인트
1
filter/exclude/order_by는 SQL을 실행하지 않는다 — 조건을 쌓기만
2
list(), for, [0] 등으로 접근할 때 비로소 SQL 실행 (lazy evaluation)
3
내부적으로 Query 객체가 조건 트리를 관리 → as_sql()로 SQL 문자열 생성
4
N+1은 select_related(JOIN) 또는 prefetch_related(별도 쿼리)로 해결
사용 사례
복잡한 검색 — 조건부 filter를 체이닝으로 조합
API 페이지네이션 — QuerySet을 슬라이싱하면 LIMIT/OFFSET SQL 생성