[실전 Django - database 정복기 5부] 인덱스(Index) 설계: "쿼리가 느리다면 답은 정해져 있다"

2025. 12. 12. 17:45·ServerDev/DJango

네, 대망의 완결편입니다. 백엔드 성능 최적화의 '끝판왕'이자, 아무리 코드를 잘 짜도 이걸 모르면 말짱 도루묵이 되는 데이터베이스 인덱스(Index)에 대한 이야기입니다. (사실 제 기준 끝판왕이지,, 새로 추가될게 언젠가는 있을 거 같습니다.. )

Django ORM은 db_index=True 옵션 하나로 인덱스를 너무 쉽게 만들 수 있게 해주지만, 그 이면에 숨겨진 작동 원리와 함정까지 알려주지는 않습니다. 쿼리 속도를 0.01초로 만드는 마지막 열쇠를 쥐어봅시다.

 


 

인덱스(Index) 설계: "쿼리가 느리다면 답은 정해져 있다"

1. 100만 건 중 1개를 찾는 방법

지난 시리즈들을 통해 우리는 N+1 문제를 해결하고, Bulk 연산으로 대량 처리를 최적화했습니다. 그런데 데이터가 100만 건, 1,000만 건으로 늘어나자 다시 API가 느려지기 시작합니다.

쿼리 로그를 보니 복잡한 JOIN도 없고, 단순한 filter 조회입니다.
SELECT * FROM users WHERE email = 'test@example.com'

이 단순한 쿼리가 왜 1초나 걸릴까요? 인덱스가 없다면 데이터베이스는 100만 개의 행을 처음부터 끝까지 하나씩 다 뒤져야(Full Table Scan) 하기 때문입니다. 마치 두꺼운 전공 책에서 '색인(Index)' 없이 특정 단어를 찾으려고 첫 페이지부터 정독하는 것과 같습니다.

오늘은 Django에서 인덱스를 제대로 거는 법, 특히 개발자들이 자주 실수하는 복합 인덱스(Composite Index)의 순서 비밀에 대해 파헤쳐 보겠습니다.

 


 

2. 기본 인덱스: db_index=True

 

 

가장 기본적인 방법입니다. 특정 컬럼으로 조회를 자주 한다면 인덱스를 걸어줍니다.

class User(models.Model):
    # 이메일로 검색할 일이 많다면?
    email = models.CharField(max_length=100, db_index=True) 

 

 

db_index=True를 설정하고 마이그레이션을 하면, DB는 이메일 컬럼을 기준으로 정렬된 별도의 목차(B-Tree)를 만듭니다. 이제 DB는 이진 탐색(Binary Search)을 통해 순식간에 데이터를 찾아낼 수 있습니다.

여기까진 누구나 압니다. 진짜 문제는 조건이 2개 이상일 때 발생합니다.

 


 

3. 복합 인덱스(Composite Index)와 함정

 

쇼핑몰 주문 내역을 조회한다고 가정해 봅시다. 보통 "특정 고객(user)"의 주문 중 최신순(created_at) 으로 조회하는 경우가 많습니다.

# 쿼리 예시
Order.objects.filter(user=my_user).order_by('-created_at')

 

이 쿼리를 빠르게 하려면 인덱스를 어떻게 걸어야 할까요?

  1. user에만 인덱스를 건다?
  2. created_at에만 인덱스를 건다?
  3. 둘 다 각각 건다?

정답은 "둘을 묶어서 하나의 복합 인덱스로 건다"입니다. Django에서는 Meta 클래스에 정의합니다.

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    status = models.CharField(max_length=10)

    class Meta:
        indexes = [
            # user와 created_at을 묶어서 인덱스 생성
            models.Index(fields=['user', 'created_at']),
        ]

 

하지만 여기서 실수가 나옵니다. 바로 컬럼의 순서(Order)입니다.

 

 


 

4. Leftmost Prefix Rule (좌측 접두사 규칙)

 

 

복합 인덱스 (A, B)를 만들었다고 해서, A로 검색하든 B로 검색하든 다 빨라지는 것이 아닙니다.

데이터베이스는 인덱스를 만들 때 첫 번째 컬럼(A)을 기준으로 먼저 정렬하고, A가 같으면 두 번째 컬럼(B)으로 정렬합니다. 마치 전화번호부와 같습니다.

 

전화번호부 예시: (성, 이름) 순으로 정렬

  • ('김', '철수')
  • ('김', '태희')
  • ('이', '민호')
  • ('박', '지성')

상황 1: 성이 '김'씨인 사람 찾기 (filter(user=...))
-> 목차 앞부분만 보면 되므로 빠릅니다. (인덱스 탐!)

상황 2: 성이 '김'씨이고 이름이 '태희'인 사람 찾기 (filter(user=..., created_at=...))
-> '김'씨 섹션으로 가서 '태희'를 찾으면 되므로 빠릅니다. (인덱스 탐!)

상황 3: 이름이 '태희'인 사람 찾기 (filter(created_at=...))
-> 성(Last Name)을 모르고 이름(First Name)만 가지고 찾으려면? 전화번호부 전체를 다 뒤져야 합니다. '김'씨 중에도 태희가 있고,

 

'이'씨 중에도 태희가 있을 수 있으니까요. 느립니다. (인덱스 안 탐! )

결론: 순서가 생명이다

fields=['user', 'created_at']으로 인덱스를 만들었다면

  • filter(user=A): 빠름
  • filter(user=A, created_at=B): 빠름
  • filter(created_at=B): 느림 (인덱스 무용지물)

 

따라서 카디널리티(Cardinality, 중복도가 낮은 정도)가 높거나, = (Equal) 조건으로 자주 조회하는 컬럼을 인덱스의 앞쪽에 배치해야 합니다.

 


5. explain()

"내 쿼리가 진짜 인덱스를 타고 있는지 어떻게 알죠?"
Django QuerySet의 explain() 메서드를 사용하면 DB가 어떤 전략을 짰는지(실행 계획) 훔쳐볼 수 있습니다.

# 인덱스를 잘 타는 경우
print(Order.objects.filter(user_id=1).explain())
# 결과 예시 (PostgreSQL):
# "Index Scan using myapp_order_user_created_at_idx on myapp_order ..."
# -> Index Scan! 성공!

# 인덱스를 못 타는 경우
print(Order.objects.filter(created_at__gte='2024-01-01').explain())
# 결과 예시:
# "Seq Scan on myapp_order ..."
# -> Seq Scan (Sequential Scan) = Full Table Scan = 망함

 

개발 단계에서 쿼리가 조금이라도 복잡하다면 반드시 explain()을 찍어보는 습관을 들여야 합니다.

 


 

6. 인덱스도 공짜는 아니다 (Trade-off)

 

 

"그럼 모든 컬럼에 인덱스를 걸면 되지 않나요?"
절대 안 됩니다. 인덱스는 책의 '맨 뒤 색인 페이지'와 같습니다.

  1. 저장 공간 차지: 인덱스도 디스크 용량을 꽤 많이 차지합니다.
  2. 쓰기 성능 저하: 데이터를 INSERT, UPDATE, DELETE 할 때마다 목차(인덱스)도 같이 수정하고 다시 정렬해야 합니다.

읽기(Read) 속도를 얻는 대신 쓰기(Write) 속도와 저장 공간을 지불하는 것입니다. 따라서 조회는 자주 일어나지만 수정은 빈번하지 않은 컬럼, 그리고 전체 데이터의 5~10% 미만으로 필터링되는 컬럼에 거는 것이 효율적입니다.

 


 

시리즈를 마치며: 백엔드 개발자의 성장

 

지금까지 총 5편에 걸쳐 [실전 Django] 시리즈를 진행했습니다.

  1. 동시성: select_for_update로 데이터 덮어쓰기 방지
  2. 조회 성능: select_related / prefetch_related로 N+1 문제 해결
  3. 쓰기 성능: bulk_create로 대용량 데이터 처리
  4. 안정성: transaction.on_commit으로 비동기 작업 타이밍 이슈 해결
  5. 검색 성능: 복합 인덱스 설계와 실행 계획 분석

 

이 5가지 기술은 단순히 Django의 기능이 아니라, 어떤 백엔드 프레임워크나 데이터베이스를 사용하더라도 통용되는 서버 개발의 핵심 원리들입니다.

로컬에서 혼자 개발할 때는 절대 알 수 없지만, 사용자가 늘어나고 데이터가 쌓이는 순간 반드시 마주치게 될 문제들이죠. 사실 해도해도 이게 맞나? 싶긴 합니다.. ㅜㅜ

 

추후 추가될것이 있다면,, 그때 다시 데이터베이스 정복기로 써보겠습니다

'ServerDev > DJango' 카테고리의 다른 글

[실전 Django - database 정복기 4부] 트랜잭션과 Celery의 미묘한 관계: "왜 알림이 안 가죠?"  (0) 2025.12.12
[실전 Django - database 정복기 3부] 대용량 데이터 처리: "for문 돌리지 말고 Bulk를 믿으세요"  (0) 2025.12.12
[실전 Django - database 정복기 2부] 기능은 똑같은데 왜 쿼리가 100방이나 나가죠? (N+1 문제 해결)  (0) 2025.12.12
[실전 Django - database 정복기 1부 ] ORM의 배신, 그리고 select_for_update로 해결한 동시성 이슈  (0) 2025.12.12
'ServerDev/DJango' 카테고리의 다른 글
  • [실전 Django - database 정복기 4부] 트랜잭션과 Celery의 미묘한 관계: "왜 알림이 안 가죠?"
  • [실전 Django - database 정복기 3부] 대용량 데이터 처리: "for문 돌리지 말고 Bulk를 믿으세요"
  • [실전 Django - database 정복기 2부] 기능은 똑같은데 왜 쿼리가 100방이나 나가죠? (N+1 문제 해결)
  • [실전 Django - database 정복기 1부 ] ORM의 배신, 그리고 select_for_update로 해결한 동시성 이슈
yeseul-kim01
yeseul-kim01
  • yeseul-kim01
    슬 개발일지
    yeseul-kim01
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 자격증 (1)
        • 정보보안기사 (0)
      • DevOps (17)
        • Docker (6)
        • Kubernetes (1)
        • GitHub Actions (0)
        • AWS (4)
        • Monitoring (1)
        • Nginx (1)
        • GCP (3)
      • ServerDev (34)
        • SpringBoot (13)
        • DJango (5)
        • FastAPI (14)
        • Next (0)
        • Flask (0)
        • Database (2)
      • Algorithm (2)
        • BFS (0)
        • DFS (1)
        • 다익스트라 (0)
      • CS (8)
      • Data Engineering (1)
      • AI&MLOps (2)
      • Architecture (6)
      • Software Engineering (0)
        • Library Packaging (0)
      • Project (5)
        • docx-generator (0)
        • speak-note (2)
        • ms-serving (1)
        • keyshield (2)
      • ProgrammingLanguages (3)
        • Python (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    동시성제어
    di
    실시간시스템
    실무일기-백엔드편
    STT
    multipartfile
    FastAPI - CORS 마스터
    Kubernetes
    아키텍처설계
    KServe
    아키텍처
    FastAPI
    Django
    docker
    SpeakNote
    MLops
    실무일기-인프라편
    depends
    NLP부트캠프
    rag
    하이브리드아키텍처
    프로젝트기록-speaknote
    grpc
    멀티모듈
    SpringBoot
    비동기처리
    트러블슈팅
    백엔드
    KeyShield
    프로젝트기록-KeyShield
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
yeseul-kim01
[실전 Django - database 정복기 5부] 인덱스(Index) 설계: "쿼리가 느리다면 답은 정해져 있다"
상단으로

티스토리툴바