이론 🔍
Instance (인스턴스)
question = Question.objects.first()
question.title / question.author → 이미 DB에서 가져온 값
Query (쿼리)
DB에게 “이런 데이터 줘”라고 요청하는 명령
- 아직 DB에 실행되진 않음 / “이런 조건의 데이터를 가져오겠다”는 요청서 수준
예시
- SQL 예시 →
SELECT * FROM question WHERE id = 1;
- Django에서의 쿼리 →
Question.objects.filter(id=1)
QuerySet (쿼리셋)
쿼리들의 집합 + 아직 실행되지 않은 결과 후보
예시
- Django에서의 쿼리 →
qs = Question.objects.filter(category=1)
qs → 리스트 ❌ / 실제 데이터 ❌ / DB에 날아간 결과 ❌
Query Parameter
URL 뒤에 붙어서 서버에 추가 조건이나 옵션을 전달하는 값
- 조회(GET) 요청에서 필터링, 정렬, 페이지네이션 같은 “조회 옵션”을 표현할 때 사용
예시
/questions?category=3&page=2&answered=true
? : Query Parameter 시작 / key=value : 하나의 파라미터 / & : 여러 개 연결
Manager (objects)
쿼리 생성기
Question.objects
.filter() / .get() / .annotate() / .select_related()
filter / get / exclude 차이
Question.objects.filter(category=1)
Question.objects.get(id=1)
Question.objects.exclude(is_deleted=True)
Django - ORM 📌
생성 → `create()` | 단건 조회 → `get()` | 필터 → `filter()` / `exclude()`
정렬 → `order_by()` | 개수 → `count()` | 존재 여부 → `exists()`
생성
create() - bulk_create() - get_or_create() - update_or_create()
조회
all() - get() - filter() - exclude() - first() - last() - values() - values_list()
수정 / 삭제
집계 / 주석
annotate() → 행 단위 계산(QuerySet)
aggregate() → 전체 요약(dict)
최적화 도구 📌
DB에 접근(hit)하는 횟수를 줄이고 더 빠르게 데이터를 조회할 수 있게 해줌
- SQL의 JOIN을 사용하여 한 번의 쿼리로 연관된 데이터를 가져옴
- 정방향 참조(정방향 Foreign Key, OneToOne) 관계에서 사용
- 추가적인 단일 쿼리를 실행한 후, 파이썬 메모리 단에서 결과를 조인
- 역방향 참조(역방향 Foreign Key) 및 다대다(ManyToMany) 관계에서 사용
Prefetch
- prefetch_related를 사용할 때
- 단순히 데이터를 가져오는 것을 넘어 추가적인 필터링이나 정렬이 필요할 때 사용하는 객체
특정값 추출
데이터베이스 수준에서 연산(통계, 집계 등)을 수행하거나 복잡한 조건의 특정 값을 추출
annotate
- 쿼리셋(QuerySet)의 각 객체에 계산된 필드(주석, Annotation)를 추가하는 기능
- SQL의 GROUP BY 및 집계 함수(COUNT, SUM, AVG 등)와 매핑되어
- 파이썬 메모리가 아닌 데이터베이스 단에서 연산을 처리
Subquery
- 메인 쿼리 안에 또 다른 쿼리(하위 쿼리)를 삽입하는 기능
- 조인(JOIN)만으로는 해결하기 어려운 복잡한 조건(예: '각 작가의 가장 최근 출판된 책의 제목' 하나만 가져오기)을
- 데이터베이스 수준에서 처리할 때 사용함 (OuterRef와 함께 짝을 이루어 사용됨)
aggregate()
- 쿼리셋(QuerySet) 전체에 대한 요약 통계(총합, 평균, 최대/최소값 등)를 계산하여
- 쿼리셋이 아닌 단일 파이썬 딕셔너리(Dictionary)를 반환
F() 객체 (F Expressions)
- 파이썬 메모리로 데이터를 가져오지 않고
- 데이터베이스 단에서 해당 모델의 특정 필드 값을 직접 참조하고 연산할 때 사용
- 경쟁 상태(Race Condition)를 방지하는 데 필수적
Window 함수 (Window Functions)
- SQL의 OVER(), PARTITION BY와 동일한 기능으로
- 행(Row)들을 특정 기준으로 그룹화(Partitioning)한 상태에서 순위(Rank), 누적합 등의
예제
모델
- 책은 하나의 작가를 가짐 (Book -> Author: 정방향 FK)
- 작가는 여러 책을 가짐 (Author -> Book: 역방향 FK, related_name='books')
- 서점은 여러 책을 팔고, 책은 여러 서점에 입점할 수 있음 (다대다 관계)
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
is_published = models.BooleanField(default=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
class Store(models.Model):
name = models.CharField(max_length=100)
books = models.ManyToManyField(Book, related_name='stores')
N+1 쿼리 문제
- 최적화를 하지 않고 모든 책과 해당 책의 작가 이름을 출력하려고 할 때 발생하는 문제
books = Book.objects.all()
for book in books:
print(f"책: {book.title}, 작가: {book.author.name}")
적용 대상: ForeignKey, OneToOneField
books = Book.objects.select_related('author').all()
for book in books:
print(f"책: {book.title}, 작가: {book.author.name}")
적용 대상: ManyToManyField, 역방향 참조(Reverse ForeignKey)
prefetch_related는 지정된 관계에 대해 단일 배치(Single batch)로 연관된 객체를 자동으로 가져옴
select_related가 한 번의 복잡한 JOIN 쿼리를 만드는 것과 달리
prefetch_related는 별도의 쿼리를 실행한 뒤 파이썬으로 데이터를 합침
authors = Author.objects.prefetch_related('books').all()
for author in authors:
print(f"작가: {author.name}")
for book in author.books.all():
print(f" - 저서: {book.title}")
해결 방법 3: Prefetch 객체 활용
Prefetch 객체는 prefetch_related()의 작동 방식을 세밀하게 제어하기 위해 사용됨
- 예를 들어, 특정 작가가 쓴 책 중에서 "출판된(
is_published=True)" 책만 가져오고 싶을 때 사용
from django.db.models import Prefetch
published_books_qs = Book.objects.filter(is_published=True)
authors = Author.objects.prefetch_related(
Prefetch(
'books',
queryset=published_books_qs,
to_attr='published_books'
)
).all()
for author in authors:
print(f"작가: {author.name}")
for book in author.published_books:
print(f" - 출판된 저서: {book.title}")
annotate 사용 예시
annotate()는 쿼리셋의 각 객체에 지정된 쿼리 표현식(Query Expressions)의 결과를 추가
- 만약 파이썬의
for문과 len() 함수를 사용해 개수를 셌다면
- 매번 쿼리가 발생하거나 메모리를 많이 차지하겠지만, annotate를 쓰면 DB에서 계산된 결과만 깔끔하게 가져옴
각 작가가 지금까지 쓴 책이 총 몇 권인지 알고 싶다
- Count 외에도 Sum, Avg, Max, Min 등 다양한 집계 함수를 사용할 수 있으며,
Count('books', filter=Q(is_published=True))처럼 특정 조건에 맞는 데이터만 집계할 수도 있음
from django.db.models import Count
authors = Author.objects.annotate(book_count=Count('books'))
for author in authors:
print(f"작가: {author.name}, 출판한 책 수: {author.book_count}권")
Subquery / OuterRef
- Subquery 클래스를 사용하면 쿼리셋 내에 명시적인 하위 쿼리를 추가할 수 있음
- 이때 하위 쿼리는 외부 쿼리(메인 쿼리)의 필드를 참조해야 하는데
- 이를 위해 OuterRef(외부 참조) 객체를 사용
각 작가의 "가장 최근에 등록된 책의 제목" 딱 1개만 작가 정보와 함께 가져오고 싶다
- Subquery는 반드시 단일 값(또는 단일 행)을 반환해야 함
- 따라서
.values('가져올_필드명')[:1]을 통해 결과가 하나만 나오도록 제한(Limit)을 걸어주는 것이 필수
from django.db.models import Subquery, OuterRef
latest_book_query = Book.objects.filter(
author=OuterRef('pk')
).order_by('-id')
authors = Author.objects.annotate(
latest_book_title=Subquery(latest_book_query.values('title')[:1])
)
for author in authors:
print(f"작가: {author.name}, 최근 저서: {author.latest_book_title}")
aggregate() : 전체 데이터 집계하기
annotate()가 쿼리셋의 '각 개별 객체'에 계산 값을 덧붙여 반환하는 반면
aggregate()는 전체 쿼리셋에 대한 단일 요약 값을 계산하는 종결(Terminal) 절
- 즉, 체이닝(Chaining)을 끝내고 최종 값을 딕셔너리 형태로 반환
도서관(또는 서점)에 있는 모든 책의 총 가격과 평균 가격을 구하고 싶다
- 파이썬에서 전체 데이터를 리스트로 가져와
sum()을 돌리는 것보다
aggregate()를 사용하여 DB 단에서 계산만 수행해 결과값 1개만 가져오는 것이 속도와 메모리 측면에서 유리
from django.db.models import Sum, Avg
book_stats = Book.objects.aggregate(
total_price=Sum('price'),
average_price=Avg('price')
)
print(f"전체 책의 총 가격: {book_stats['total_price']}원")
print(f"전체 책의 평균 가격: {book_stats['average_price']}원")
F() 객체 : DB 수준의 필드 참조 및 연산
- F() 표현식은 모델 필드의 값을 나타냄
- 데이터베이스에서 파이썬 메모리로 데이터를 실제로 가져오지 않고도
- 모델 필드의 값을 참조하고 이를 사용해 데이터베이스 연산을 수행할 수 있게 해줌
[상황 1: 연산] 모든 책의 가격을 1,000원씩 인상해야 합니다.
[상황 2: 비교] (할인가 필드가 있다고 가정할 때)
원가보다 할인가가 높은(잘못 입력된) 책을 찾고 싶습니다.
- 여러 사용자가 동시에 같은 상품의 재고를 감소시키는 로직에서 stock = stock - 1을
- 파이썬 단에서 처리하면 꼬일 수 있습니다(Race Condition).
- 이때
update(stock=F('stock') - 1)을 사용하면
- 데이터베이스가 락(Lock)을 관리하여 안전하게 값이 변경됨
from django.db.models import F
Book.objects.all().update(price=F('price') + 1000)
error_books = Book.objects.filter(discount_price__gt=F('price'))
for book in error_books:
print(f"오류 데이터: {book.title} (원가: {book.price}, 할인가: {book.discount_price})")
Window 함수 : 그룹 내 고급 분석
- Window 표현식을 사용하면 전체 데이터를 여러 파티션으로 나누고
- 그 파티션 내에서 순위나 누적합을 계산할 수 있음
annotate()와 함께 사용됨
각 작가(Author)별로 가장 비싼 책부터 순위(Rank)를 매기고 싶습니다.
- Window 함수는 MySQL 8.0 이상, PostgreSQL, Oracle 등 최신 관계형 데이터베이스에서 주로 지원
- 복잡한 통계 화면(대시보드)을 개발할 때 파이썬 코드를 수십 줄 줄여주는 기능
from django.db.models import F, Window
from django.db.models.functions import Rank
books_with_rank = Book.objects.annotate(
price_rank_per_author=Window(
expression=Rank(),
partition_by=[F('author_id')],
order_by=F('price').desc(),
)
)
for book in books_with_rank:
print(f"작가 ID: {book.author_id},
책: {book.title}, 가격: {book.price},
작가 내 가격 순위: {book.price_rank_per_author}위")