프로그래밍/Python

🐌 Django에서 쿼리가 느린 진짜 이유: 인덱스 누락 문제 해결하기

Tiboong 2025. 9. 16. 15:00
728x90
반응형

"개발할 때는 빨랐는데 실제 서비스에서는 왜 이렇게 느리죠?"

이런 질문을 정말 많이 받습니다. 로컬에서 테스트할 때는 데이터가 몇 개 없어서 빠르지만, 실제 서비스에는 수만 개, 수십만 개의 데이터가 쌓여있죠.

그때 갑자기 웹사이트가 거북이처럼 느려집니다. 사용자들은 10초씩 기다리다가 떠나고, 서버는 과부하로 다운되기 시작합니다.

이 모든 문제의 원인 중 80%는 바로 '데이터베이스 인덱스'를 제대로 이해하지 못해서 생깁니다.

오늘은 Django 개발자라면 반드시 알아야 할 데이터베이스 인덱스에 대해 쉽게 설명해드리겠습니다.

🤔 인덱스가 뭔지 쉽게 이해해보기

1. 일상 속 인덱스 - 책의 찾아보기

여러분이 1000페이지짜리 요리책에서 "김치찌개" 레시피를 찾는다고 생각해보세요.

방법 1: 처음부터 끝까지 찾기

  • 1페이지부터 시작해서 한 장씩 넘기며 "김치찌개"를 찾습니다
  • 운이 나쁘면 999페이지에서 찾을 수도 있어요
  • 평균적으로 500페이지 정도는 넘겨야 합니다

방법 2: 책 뒤의 찾아보기(색인) 활용

  • 맨 뒤 찾아보기에서 "김치찌개 - 347페이지" 확인
  • 바로 347페이지로 이동
  • 1-2초 만에 찾기 완료!

이때 "찾아보기"가 바로 인덱스입니다!

2. 데이터베이스도 똑같습니다

인덱스 없이 데이터 찾기:

사용자 100만 명 중에서 이메일이 "john@example.com"인 사람 찾기
→ 데이터베이스: "1번 사용자부터 100만번 사용자까지 하나씩 확인해야겠다..."
→ 결과: 평균 50만 번 확인 → 몇 초에서 몇십 초 소요

 

인덱스 있으면:

이메일 인덱스 확인
→ "john@example.com은 사용자 ID 12345번이군!"
→ 바로 해당 데이터 반환
→ 결과: 0.001초 만에 완료

🔍 인덱스의 내부 동작 원리

B-Tree: 인덱스가 데이터를 정리하는 방법

대부분의 데이터베이스는 B-Tree라는 구조로 인덱스를 만듭니다. 이건 마치 거대한 이진 트리 같은 구조예요.

예시: 사용자 이름으로 만든 인덱스

                [M]
              /     \
         [C, G]     [Q, T]
        /  |  \    /  |   \
    [A,B] [E,F] [J,L] [P] [R,S] [V,Z]

"John"을 찾는 과정:

  1. 루트(M)에서 시작: J < M 이므로 왼쪽으로
  2. [C,G]에서: C < J < G 이므로 가운데로
  3. [J,L]에서: John 찾음!

결과: 100만 개 데이터라도 최대 20번 정도의 비교만으로 찾을 수 있습니다!

왜 이렇게 빠를까요?

선형 검색 (인덱스 없음):

  • 데이터가 2배 늘면 검색 시간도 2배
  • 1만 건 → 평균 5,000번 비교
  • 100만 건 → 평균 500,000번 비교 (100배 더 오래 걸림!)

B-Tree 검색 (인덱스 있음):

  • 데이터가 2배 늘어도 비교 횟수는 1-2번만 증가
  • 1만 건 → 약 14번 비교
  • 100만 건 → 약 20번 비교 (1.4배만 더 걸림!)

이것이 **로그 시간 복잡도(O(log n))**의 힘입니다.

📊 실제로 얼마나 차이날까요?

실제 성능 테스트

사용자 100만 명이 등록된 실제 서비스에서 테스트해본 결과입니다:

테스트 쿼리: 특정 이메일로 사용자 찾기

❌ 인덱스 없을 때:
   - 평균 실행 시간: 8.2초
   - 데이터베이스 CPU 사용률: 90%
   - 사용자 경험: "왜 이렇게 느려요?" 😠

✅ 인덱스 있을 때:
   - 평균 실행 시간: 0.003초  
   - 데이터베이스 CPU 사용률: 5%
   - 사용자 경험: "와, 빠르네요!" 😊

성능 향상: 2,700배!

🎯 어떤 필드에 인덱스를 만들어야 할까요?

인덱스가 꼭 필요한 경우들

1. 로그인 관련 필드

  • 사용자 이름, 이메일처럼 자주 검색하는 필드
  • "특정 사용자"를 찾는 일이 매우 자주 발생

2. 외래키 (Foreign Key)

  • 다른 테이블과 연결되는 필드들
  • JOIN 연산에서 성능에 큰 영향을 미침

3. 자주 필터링하는 필드

  • "활성 사용자만 보기", "카테고리별 상품 보기" 같은 조건
  • WHERE 절에 자주 사용되는 필드들

4. 정렬 기준이 되는 필드

  • "최신 글 순서", "가격 순서" 같은 정렬
  • ORDER BY에 사용되는 필드들

5. 날짜/시간 필드

  • "오늘 가입한 사용자", "이번 달 주문" 같은 범위 검색
  • 시간 순서로 정렬하는 경우가 많음

인덱스가 별로 도움 안 되는 경우들

1. 값의 종류가 너무 적은 필드

  • 성별(남/여), 상태(Y/N) 같이 선택지가 2-3개뿐인 경우
  • 전체 데이터의 50%가 같은 값이면 인덱스 효과가 제한적

2. 자주 변경되는 필드

  • 조회수, 좋아요 수처럼 계속 업데이트되는 필드
  • 인덱스도 함께 업데이트되어야 해서 오히려 느려질 수 있음

3. 텍스트 검색용 필드

  • 긴 텍스트 내용에서 단어 검색하는 경우
  • 일반 인덱스보다는 전문 검색 엔진이 더 적합

🛠️ Django에서 인덱스 만드는 방법

방법 1: 간단한 단일 필드 인덱스

가장 쉬운 방법은 필드 정의할 때 db_index=True를 추가하는 것입니다:

class User(models.Model):
    username = models.CharField(max_length=150, db_index=True)  # 인덱스 추가!
    email = models.EmailField(db_index=True)                   # 인덱스 추가!
    phone = models.CharField(max_length=20)                    # 인덱스 없음

이렇게 하면 Django가 자동으로 해당 필드에 인덱스를 만들어줍니다.

방법 2: 여러 필드를 조합한 복합 인덱스

실제 서비스에서는 여러 조건을 함께 사용하는 경우가 많습니다. 예를 들어 "특정 카테고리의 활성 상품들"을 찾는다면:

class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.CharField(max_length=50)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        indexes = [
            # 카테고리 + 활성 상태 조합 인덱스
            models.Index(fields=['category', 'is_active'], name='product_cat_active_idx'),
            # 카테고리 + 생성일 조합 인덱스  
            models.Index(fields=['category', '-created_at'], name='product_cat_date_idx'),
        ]

 

복합 인덱스의 순서가 중요한 이유:

전화번호부를 생각해보세요. "성(姓) + 이름" 순서로 정렬되어 있죠?

  • "김씨 찾기" → 쉬움
  • "철수 찾기" → 어려움 (모든 성씨의 철수를 다 찾아야 함)

데이터베이스 인덱스도 마찬가지입니다!

# 인덱스: ['category', 'is_active'] 순서

# ✅ 효과적인 쿼리들
Product.objects.filter(category='전자제품')  # category만: 좋음
Product.objects.filter(category='전자제품', is_active=True)  # 둘 다: 최고!

# ❌ 비효과적인 쿼리  
Product.objects.filter(is_active=True)  # is_active만: 인덱스 활용 어려움

🚨 인덱스 사용할 때 꼭 알아야 할 주의사항

인덱스가 항상 좋은 건 아닙니다!

인덱스의 단점들:

1. 저장 공간을 많이 사용합니다

원본 테이블 크기: 1GB
인덱스들 크기: 200-300MB 추가 사용
→ 전체 30% 정도 공간 추가 필요

 

2. 데이터 입력/수정/삭제가 느려집니다

새 사용자 가입 시 실제 일어나는 일:
1. users 테이블에 데이터 삽입
2. username 인덱스 업데이트
3. email 인덱스 업데이트  
4. created_at 인덱스 업데이트
→ 인덱스가 많을수록 가입 처리가 느려짐!

 

3. 잘못된 인덱스는 오히려 성능을 떨어뜨립니다

언제 인덱스를 만들지 말아야 할까요?

1. 값의 종류가 너무 적은 경우

class User(models.Model):
    gender = models.CharField(max_length=1)  # 'M' 또는 'F'만 있음
    # 이런 필드에 인덱스 만들면 비효율적!
    # 전체 사용자의 50%가 같은 값이라서 별로 도움 안 됨

 

2. 자주 변경되는 테이블

class ViewCount(models.Model):
    # 초당 100번씩 업데이트되는 테이블
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    count = models.IntegerField()  # 계속 변경됨
    # 너무 많은 인덱스는 업데이트 성능을 크게 저하시킴

3. 너무 많은 인덱스

  • 테이블당 5-10개 이상의 인덱스는 신중하게 고려
  • 실제로 사용되지 않는 인덱스는 공간만 차지

🔧 실전에서 인덱스 활용하기

실제 사례: 쇼핑몰 상품 검색

상황: 사용자가 상품을 검색할 때 자주 사용하는 패턴들

  • "전자제품 카테고리의 활성 상품들"
  • "특정 브랜드의 10만원 이하 상품들"
  • "최신 등록 순서로 상품 보기"

Before: 인덱스 없는 상태

class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.CharField(max_length=50)
    brand = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

# 자주 사용되는 쿼리들
Product.objects.filter(category='전자제품', is_active=True)  # 3.2초 소요
Product.objects.filter(brand='삼성', price__lte=100000)    # 2.8초 소요
Product.objects.order_by('-created_at')[:20]              # 1.9초 소요

After: 적절한 인덱스 추가

class Product(models.Model):
    name = models.CharField(max_length=200, db_index=True)  # 상품명 검색용
    category = models.CharField(max_length=50)
    brand = models.CharField(max_length=100, db_index=True)  # 브랜드 검색용
    price = models.DecimalField(max_digits=10, decimal_places=2, db_index=True)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)  # 정렬용
    
    class Meta:
        indexes = [
            # 가장 자주 사용되는 조합
            models.Index(fields=['category', 'is_active', '-created_at'], 
                        name='product_category_active_date_idx'),
            # 브랜드 + 가격 검색용
            models.Index(fields=['brand', 'price'], name='product_brand_price_idx'),
        ]

# 같은 쿼리들의 성능
Product.objects.filter(category='전자제품', is_active=True)  # 0.015초 (213배 개선!)
Product.objects.filter(brand='삼성', price__lte=100000)    # 0.023초 (122배 개선!)
Product.objects.order_by('-created_at')[:20]              # 0.008초 (238배 개선!)

🎯 정리하면서

꼭 기억해야 할 핵심 포인트

1. 인덱스는 검색 속도를 극적으로 향상시킵니다

  • 수천 배에서 수만 배까지 성능 개선 가능
  • 특히 데이터가 많을수록 효과가 커집니다

2. 모든 필드에 인덱스를 만들 필요는 없습니다

  • WHERE 절에 자주 사용되는 필드 우선
  • ORDER BY에 사용되는 필드 고려
  • 외래키는 거의 필수

3. 복합 인덱스의 순서가 매우 중요합니다

  • 자주 단독으로 검색하는 필드를 앞에
  • 전화번호부의 "성 + 이름" 순서와 같은 원리

4. 인덱스에도 비용이 있습니다

  • 저장 공간 추가 사용
  • 데이터 입력/수정 시 성능 저하
  • 너무 많으면 오히려 역효과

5. 실제 사용 패턴을 분석해서 만드세요

  • 개발자가 예상하는 사용 패턴 ≠ 실제 사용자 패턴
  • 서비스 운영 후 로그 분석해서 조정

마지막으로

인덱스는 데이터베이스 성능 최적화의 핵심입니다. 하지만 무작정 많이 만든다고 좋은 게 아니라, "언제, 어디에, 왜" 만들어야 하는지 이해하는 것이 중요합니다.

처음에는 꼭 필요한 곳에만 만들고, 서비스 운영하면서 점진적으로 최적화해나가는 것을 추천합니다.


💬 여전히 데이터베이스가 느려서 고민이신가요?

복잡한 쿼리들이 왜 느린지, 어떤 인덱스를 추가해야 할지 모르겠다고요?

27년 경력의 시니어 개발자가 여러분의 Django 프로젝트를 직접 분석하고 최적화해드립니다.

  • 🔍 현재 데이터베이스 성능 전체 진단
  • ⚡ 최적의 인덱스 설계 및 적용
  • 📊 쿼리 성능 모니터링 시스템 구축
  • 🎓 팀 개발자 대상 실무 교육

➡️ 데이터베이스 최적화 전문가 상담받기

"느린 데이터베이스 때문에 밤잠 설치지 마세요. 전문가가 근본적으로 해결해드립니다!"

728x90
반응형