ORM - Query

김기훈·2026년 3월 27일

이론

목록 보기
10/10

이론 🔍


Instance (인스턴스)

  • 테이블의 “한 row”

# question = 인스턴스 → 실제 DB 데이터 1줄
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()
        • 전부 QuerySet을 만들어내는 역할
  • filter / get / exclude 차이

# 결과: QuerySet | 0개여도 에러 ❌
Question.objects.filter(category=1)

# 결과: Instance | 0개 ❌ / 2개 이상 ❌ → 에러
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()

수정 / 삭제

  • update() - delete()

집계 / 주석

  • 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 쿼리 문제

  • 최적화를 하지 않고 모든 책과 해당 책의 작가 이름을 출력하려고 할 때 발생하는 문제
# [문제 상황] 모든 책을 가져옴  (Query 1번 발생)
books = Book.objects.all()

for book in books:
    # book.author.name을 호출할 때마다 Author 테이블을 조회함
    # 책이 100권이라면 작가를 찾기 위해 Query가 100번 추가로 발생 (N번)
    # 총 101번의 쿼리 발생 (N+1 문제)
    print(f"책: {book.title}, 작가: {book.author.name}")

  • 적용 대상: ForeignKey, OneToOneField

# JOIN을 사용하여 Book과 Author 데이터를 한 번의 쿼리로 모두 가져옴
books = Book.objects.select_related('author').all()

for book in books:
    # 이미 캐싱된 데이터를 사용하므로, 여기서 추가 쿼리가 발생하지 않음
    # 총 1번의 쿼리만 발생
    print(f"책: {book.title}, 작가: {book.author.name}")

# 실제 실행되는 SQL (예시):
# SELECT "book"."id", "book"."title", "author"."id", "author"."name" 
# FROM "book" INNER JOIN "author" ON ("book"."author_id" = "author"."id");

  • 적용 대상: 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}")
    # author.books.all()을 호출할 때 추가 쿼리가 발생하지 않음
    for book in author.books.all():
        print(f" - 저서: {book.title}")

# 실제 실행되는 SQL (2번의 쿼리 발생):
# 1. SELECT "author"."id", "author"."name" FROM "author";
# 2. SELECT "book"."id", "book"."title", "book"."author_id" FROM 
# 													"book" WHERE "book"."author_id" IN (1, 2, 3...);

해결 방법 3: Prefetch 객체 활용

  • Prefetch 객체는 prefetch_related()의 작동 방식을 세밀하게 제어하기 위해 사용됨
    • 예를 들어, 특정 작가가 쓴 책 중에서 "출판된(is_published=True)" 책만 가져오고 싶을 때 사용
from django.db.models import Prefetch

# Book 모델에서 출판된 책만 필터링하는 QuerySet을 미리 정의함
published_books_qs = Book.objects.filter(is_published=True)

# Prefetch 객체를 생성하여 prefetch_related 안에 넣어줌
authors = Author.objects.prefetch_related(
    Prefetch(
        'books', # prefetch할 대상 (역방향 참조 이름)
        queryset=published_books_qs, # 미리 정의한 커스텀 QuerySet 적용
        to_attr='published_books' # (선택 사항) 결과를 저장할 새로운 속성 이름 지정
    )
).all()

for author in authors:
    print(f"작가: {author.name}")
    
    # to_attr='published_books'를 사용했으므로 .all()을 붙이지 않고 바로 리스트로 순회
    # 추가 쿼리 없이 출판된 책 목록만 가져옴
    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

# Author 쿼리셋의 각 객체에 'book_count'라는 새로운 속성을 계산하여 붙여줌(annotate).
authors = Author.objects.annotate(book_count=Count('books'))

for author in authors:
    # 파이썬에서 계산한 것이 아니라, DB에서 이미 숫자를 세어서 가져옴
    # 추가적인 쿼리 발생 없이 바로 값을 출력
    print(f"작가: {author.name}, 출판한 책 수: {author.book_count}권")

# 실제 데이터베이스에서 실행되는 SQL (예시):
# SELECT "author"."id", "author"."name", COUNT("book"."id") AS "book_count" 
# FROM "author" LEFT OUTER JOIN "book" ON ("author"."id" = "book"."author_id") 
# GROUP BY "author"."id", "author"."name";

Subquery / OuterRef

  • Subquery 클래스를 사용하면 쿼리셋 내에 명시적인 하위 쿼리를 추가할 수 있음
    • 이때 하위 쿼리는 외부 쿼리(메인 쿼리)의 필드를 참조해야 하는데
    • 이를 위해 OuterRef(외부 참조) 객체를 사용
  • 각 작가의 "가장 최근에 등록된 책의 제목" 딱 1개만 작가 정보와 함께 가져오고 싶다

    • Subquery는 반드시 단일 값(또는 단일 행)을 반환해야 함
    • 따라서 .values('가져올_필드명')[:1]을 통해 결과가 하나만 나오도록 제한(Limit)을 걸어주는 것이 필수
from django.db.models import Subquery, OuterRef

# 1. 하위 쿼리(Subquery) 정의: 특정 작가의 가장 최근 책 하나를 찾는 쿼리
# OuterRef('pk')는 이 하위 쿼리를 감싸게 될 메인 쿼리(Author)의 Primary Key(id)를 의미합니다.
latest_book_query = Book.objects.filter(
    author=OuterRef('pk')  # 외부 쿼리의 작가 id와 일치하는 책만 필터링
).order_by('-id')  # 최신순 정렬 (id가 높거나 created_at 필드가 있다면 -created_at 사용)

# 2. 메인 쿼리에 Subquery 결합
# Author 객체들을 가져오면서, 'latest_book_title'이라는 필드에 위의 하위 쿼리 결과 중 'title' 값 하나만 쏙 뽑아서 붙여줌
authors = Author.objects.annotate(
    latest_book_title=Subquery(latest_book_query.values('title')[:1])
)

for author in authors:
    # 각 작가마다 가장 최근 책의 제목이 붙어있음, 책이 없다면 None이 반환
    print(f"작가: {author.name}, 최근 저서: {author.latest_book_title}")

# 실제 데이터베이스에서 실행되는 SQL (예시):
# SELECT "author"."id", "author"."name", 
#   (SELECT U0."title" FROM "book" U0 WHERE U0."author_id" = ("author"."id") 
# ORDER BY U0."id" DESC LIMIT 1) AS "latest_book_title" 
# FROM "author";

aggregate() : 전체 데이터 집계하기

  • annotate()가 쿼리셋의 '각 개별 객체'에 계산 값을 덧붙여 반환하는 반면
    • aggregate()는 전체 쿼리셋에 대한 단일 요약 값을 계산하는 종결(Terminal) 절
    • 즉, 체이닝(Chaining)을 끝내고 최종 값을 딕셔너리 형태로 반환
  • 도서관(또는 서점)에 있는 모든 책의 총 가격과 평균 가격을 구하고 싶다

    • 파이썬에서 전체 데이터를 리스트로 가져와 sum()을 돌리는 것보다
    • aggregate()를 사용하여 DB 단에서 계산만 수행해 결과값 1개만 가져오는 것이 속도와 메모리 측면에서 유리
from django.db.models import Sum, Avg

# Book 테이블 전체를 대상으로 price 필드의 총합과 평균을 구합니다.
# 반환값은 QuerySet이 아니라 Dictionary입니다.
book_stats = Book.objects.aggregate(
    total_price=Sum('price'),  # 모든 책 가격의 총합
    average_price=Avg('price') # 모든 책 가격의 평균
)

# 반환된 딕셔너리에서 값을 바로 꺼내어 사용합니다.
# 출력 예시: {'total_price': 150000, 'average_price': 15000.0}
print(f"전체 책의 총 가격: {book_stats['total_price']}원")
print(f"전체 책의 평균 가격: {book_stats['average_price']}원")

# 실제 데이터베이스에서 실행되는 SQL:
# SELECT SUM("book"."price") AS "total_price", AVG("book"."price") AS "average_price" FROM "book";

F() 객체 : DB 수준의 필드 참조 및 연산

  • F() 표현식은 모델 필드의 값을 나타냄
    • 데이터베이스에서 파이썬 메모리로 데이터를 실제로 가져오지 않고도
    • 모델 필드의 값을 참조하고 이를 사용해 데이터베이스 연산을 수행할 수 있게 해줌
  • [상황 1: 연산] 모든 책의 가격을 1,000원씩 인상해야 합니다.

    • [상황 2: 비교] (할인가 필드가 있다고 가정할 때)

      • 원가보다 할인가가 높은(잘못 입력된) 책을 찾고 싶습니다.

    • 여러 사용자가 동시에 같은 상품의 재고를 감소시키는 로직에서 stock = stock - 1을
      • 파이썬 단에서 처리하면 꼬일 수 있습니다(Race Condition).
      • 이때 update(stock=F('stock') - 1)을 사용하면
      • 데이터베이스가 락(Lock)을 관리하여 안전하게 값이 변경됨
from django.db.models import F

# [F() 객체 사용 예시 1: 일괄 업데이트]
# Python 메모리로 데이터를 불러오지 않고, DB 내에서 price = price + 1000 연산을 즉시 실행합니다.
Book.objects.all().update(price=F('price') + 1000)

# 실제 실행되는 SQL:
# UPDATE "book" SET "price" = ("book"."price" + 1000);

# [F() 객체 사용 예시 2: 필드 간 비교]
# 동일한 테이블 내의 'discount_price' 필드와 'price' 필드의 값을 비교합니다.
# (discount_price 필드가 모델에 존재한다고 가정)
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

# [Window 함수 사용 예시]
# 각 책(Book) 객체에 'price_rank_per_author'라는 순위 속성을 달아줍니다.
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}위")

# 실제 실행되는 SQL (PostgreSQL 등 윈도우 함수 지원 DB):
# SELECT "book"."id", "book"."title", "book"."price", "book"."author_id",
#   RANK() OVER (PARTITION BY "book"."author_id" ORDER BY "book"."price" DESC) 
# 																	AS "price_rank_per_author"
# FROM "book";

profile
안녕하세요.

0개의 댓글