2025년 11월 4일 작성

MongoDB Compound Index - 여러 Field를 포함하는 색인

Compound Index는 여러 field를 함께 indexing하여 복합 query의 성능을 대폭 향상시킵니다.

Compound Index : 다중 Field 검색을 위한 색인

  • compound index는 두 개 이상의 field를 함께 indexing하는 특수한 index입니다.
    • 여러 field의 조건을 동시에 만족하는 document를 효율적으로 찾을 수 있습니다.
  • compound index는 단일 field index와 달리 field의 순서가 매우 중요합니다.
    • index의 첫 번째 field부터 순차적으로 검색하므로, query pattern에 맞게 field 순서를 설계해야 합니다.
  • MongoDB는 single index로도 여러 field query를 처리할 수 있지만, compound index를 사용하면 훨씬 더 효율적입니다.
    • 특히 sort(), 범위 검색, 여러 조건의 AND query에서 성능이 크게 향상됩니다.

Compound Index의 필요성

  • compound index는 multiple field query의 성능을 획기적으로 개선하는 가장 효과적인 방법입니다.
    • 단일 index로는 여러 조건을 동시에 처리하기 어렵고, full collection scan이 발생할 수 있습니다.

단일 Index의 한계

  • 각 field마다 별도의 단일 index를 생성해도 MongoDB는 한 번에 하나의 index만 사용합니다.
    • 여러 field 조건이 있는 query에서는 먼저 선택된 index로 document를 찾은 후, 나머지 조건을 memory에서 filtering합니다.
// 두 개의 단일 index
db.users.createIndex({ age: 1 });
db.users.createIndex({ status: 1 });

// query에서는 한 가지 index만 선택됨
db.users.find({ age: 25, status: "active" });
// age index를 선택하고, status는 memory에서 filtering
  • 이런 방식은 filtering할 document가 많으면 매우 비효율적입니다.

Compound Index의 장점

  • compound index를 사용하면 두 field 모두에 대한 index를 활용할 수 있습니다.
// compound index
db.users.createIndex({ age: 1, status: 1 });

// query 실행
db.users.find({ age: 25, status: "active" });
// age와 status 모두를 index로 탐색
  • 첫 번째 field로 빠르게 후보를 줄인 후, 두 번째 field로 추가 filtering합니다.
    • 두 번째 field로 정렬된 상태로 document를 반환하므로 sort() operation도 생략됩니다.

Compound Index 생성

  • compound index는 여러 field를 객체 형태로 지정하여 생성합니다.

기본 Compound Index

// age와 status에 대한 compound index
db.users.createIndex({ age: 1, status: 1 });
  • field 다음의 1은 ascending order, -1은 descending order를 의미합니다.
    • index 생성 시 정렬 방향을 명시해야 합니다.
  • 최대 32개의 field를 하나의 compound index에 포함할 수 있습니다.

정렬 방향 지정

// age는 ascending, status는 descending
db.users.createIndex({ age: 1, status: -1 });

// query 실행 시 age는 낮은 순, status는 높은 순으로 정렬됨
db.users.find({ age: 25 }).sort({ age: 1, status: -1 });
  • compound index의 정렬 방향은 query의 정렬 조건과 일치할 때 가장 효율적입니다.

복잡한 Compound Index

// 3개 이상의 field를 포함하는 compound index
db.orders.createIndex({
    customerId: 1,
    orderDate: -1,
    status: 1
});

// 다양한 query pattern을 지원
db.orders.find({ customerId: "123" });
db.orders.find({ customerId: "123", orderDate: { $gte: new Date("2025-01-01") } });
db.orders.find({ customerId: "123", orderDate: -1, status: "completed" });
  • field가 많을수록 다양한 query를 지원할 수 있지만, write 성능에는 영향을 줍니다.

ESR Rule : Compound Index 설계 원칙

  • compound index를 효율적으로 설계하기 위해 ESR rule을 따르는 것이 권장됩니다.
    • ESR은 Equality, Sort, Range의 약자입니다.

ESR Rule의 의미

  • Equality : 정확한 일치(=)를 확인하는 field를 맨 앞에 배치합니다.
    • { status: "active" } 같은 조건을 처리하는 field입니다.
  • Sort : 정렬(sort())이 필요한 field를 중간에 배치합니다.
    • 정렬된 순서로 document를 반환하는 field입니다.
  • Range : 범위 검색(<, >, >=, <=)을 하는field를 마지막에 배치합니다.
    • { age: { $gte: 20 } } 같은 범위 조건을 처리하는 field입니다.

ESR Rule 적용 예시

// 나쁜 예 : field 순서가 뒤바뀜
db.users.createIndex({ age: 1, status: 1, createdAt: -1 });

// 좋은 예 : ESR rule 적용
// equality: status, sort: createdAt, range: age
db.users.createIndex({ status: 1, createdAt: -1, age: 1 });

// query
db.users.find({
    status: "active",
    age: { $gte: 20 }
}).sort({ createdAt: -1 });
  • ESR rule을 따르면 query 실행 시 모든 index를 활용할 수 있습니다.
    • field 순서가 틀리면 일부 field는 memory에서 filtering됩니다.

Compound Index 활용

  • compound index는 다양한 query pattern을 지원합니다.

Prefix 사용

// compound index
db.users.createIndex({ country: 1, age: 1, name: 1 });

// 다음 query들은 모두 이 index를 활용
db.users.find({ country: "Korea" });
db.users.find({ country: "Korea", age: 25 });
db.users.find({ country: "Korea", age: 25, name: "John" });

// 다음 query는 index를 활용하지 못함 (age가 먼저 나옴)
db.users.find({ age: 25, name: "John" });
  • compound index는 field의 prefix를 활용할 수 있습니다.
    • 앞의 field부터 순차적으로 사용되므로, 첫 번째 field부터 시작하는 query들이 index를 활용합니다.

Sort 최적화

// index
db.users.createIndex({ status: 1, age: 1 });

// in-memory sort 없음 (index에 이미 정렬됨)
db.users.find({ status: "active" }).sort({ age: 1 });

// in-memory sort 필요 (역순 정렬)
db.users.find({ status: "active" }).sort({ age: -1 });
  • compound index의 정렬 방향과 query의 정렬 방향이 일치하면 추가 정렬이 필요 없습니다.

Covered Query

// compound index (projection도 포함)
db.users.createIndex({ status: 1, age: 1 });

// covered query: document 접근 없음
db.users.find(
    { status: "active", age: 25 },
    { _id: 0, status: 1, age: 1 }
);
  • compound index의 모든 field가 projection에 포함되면 covered query가 가능합니다.
    • covered query란 index만으로 필요한 data를 모두 제공하는 query를 의미합니다.
    • document를 읽을 필요 없이 index만으로 query를 처리합니다.

Compound Index vs 단일 Index

  • compound index와 여러 개의 단일 index의 차이를 비교합니다.

성능 비교

// 단일 index 두 개
db.orders.createIndex({ customerId: 1 });
db.orders.createIndex({ status: 1 });

// query 실행 - 하나의 index만 선택됨
db.orders.find({ customerId: "123", status: "completed" });
// customerId index로 찾은 후 status는 memory에서 filtering

// compound index 하나
db.orders.createIndex({ customerId: 1, status: 1 });

// query 실행 - 두 field 모두 index 활용
db.orders.find({ customerId: "123", status: "completed" });
// customerId와 status 모두 index로 탐색
  • compound index는 두 field의 조건을 모두 index에서 처리합니다.
    • 단일 index는 하나만 사용되고 나머지는 memory에서 처리됩니다.

저장 공간

// 단일 index 두 개 : 각각 저장 공간 사용
db.orders.createIndex({ customerId: 1 });
db.orders.createIndex({ status: 1 });

// compound index 하나 : 더 효율적인 저장
db.orders.createIndex({ customerId: 1, status: 1 });
  • compound index가 여러 단일 index보다 저장 공간을 절약할 수 있습니다.
    • 하지만 동일한 field를 여러 index에서 중복으로 처리하면 저장 공간이 증가합니다.

Compound Index 성능 최적화

  • compound index를 효율적으로 사용하기 위한 전략이 있습니다.
    • field 선택과 정렬 방향 설계가 성능을 크게 좌우하므로, 신중히 결정해야 합니다.

자주 사용되는 Query 분석

// 자주 실행되는 query pattern
db.products.find({ category: "electronics", price: { $lt: 100 } });
db.products.find({ category: "electronics", popularity: -1 });
db.products.find({ category: "electronics", inStock: true });

// 이 pattern들을 모두 지원하려면
db.products.createIndex({ category: 1, inStock: 1, price: 1, popularity: -1 });
  • 자주 사용되는 query pattern을 파악한 후 field 순서를 결정해야 합니다.

Write Performance 고려

// compound index가 많으면 write 성능 저하
db.users.createIndex({ email: 1 });
db.users.createIndex({ username: 1 });
db.users.createIndex({ status: 1, createdAt: -1 });
db.users.createIndex({ email: 1, status: 1 });
// ... 더 많은 index

// document 삽입 시 모든 index를 update해야 함
db.users.insertOne({ email: "test@example.com", username: "test", status: "active", createdAt: new Date() });
  • 너무 많은 compound index는 write operation을 느리게 만듭니다.
    • 필요한 query pattern만 선택적으로 index를 생성해야 합니다.

Index Size Monitoring

// index 크기 확인
db.users.stats().indexSizes;

// 사용되지 않는 index 제거
db.users.dropIndex({ email: 1, status: 1 });
  • 자주 사용되지 않는 compound index는 제거하여 저장 공간과 write 성능을 개선합니다.

Compound Index 제약 사항

  • compound index는 여러 제약 사항을 고려하여 설계해야 합니다.

Field 순서의 중요성

  • field 순서가 다르면 다른 index로 취급됩니다.
// 서로 다른 index
db.users.createIndex({ status: 1, age: 1 });
db.users.createIndex({ age: 1, status: 1 });

// 첫 번째 query는 첫 번째 index 사용
db.users.find({ status: "active" });

// 두 번째 query는 두 번째 index 사용
db.users.find({ age: 25 });

// 역순 정렬이 필요한 경우
db.users.find({ status: "active" }).sort({ age: -1 });
// age가 descending인 index를 따로 생성해야 함
  • query pattern을 신중히 분석하여 field 순서를 결정해야 합니다.

Sort 방향 제약

// index
db.users.createIndex({ status: 1, age: 1 });

// 일치 : index 방향과 query 정렬 일치
db.users.find({ status: "active" }).sort({ age: 1 });
// → status로 filtering (index 사용) + age 정렬 (index에 이미 정렬됨)

// 불일치 : index 방향과 query 정렬 역순
db.users.find({ status: "active" }).sort({ age: -1 });
// → status로 filtering (index 사용) + age 역순 정렬 (memory에서 재정렬 필요)
  • index는 filtering에는 사용되지만, 정렬 방향이 다르면 in-memory sort가 발생합니다.
    • 정렬 방향이 일치할 때만 index에서 이미 정렬된 순서를 활용할 수 있습니다.

범위 검색 후 정렬

// 나쁜 설계 : 범위 검색이 정렬 field 앞에 위치
db.orders.createIndex({ status: 1, amount: 1, date: -1 });
db.orders.find({
    status: "completed",
    amount: { $gte: 100 }
}).sort({ date: -1 });
// status로 filtering → amount 범위 검색 → date로 memory에서 정렬 필요
// amount 범위 이후로 index의 date 정렬 순서를 활용할 수 없음

// 좋은 설계 : ESR rule 적용 (Equality → Sort → Range)
db.orders.createIndex({ status: 1, date: -1, amount: 1 });
db.orders.find({
    status: "completed",
    amount: { $gte: 100 }
}).sort({ date: -1 });
// status로 filtering (Equality) → date로 정렬 (Sort, index에 이미 정렬됨) → amount 범위 (Range)
// 모든 작업을 index에서 처리
  • 범위 검색과 정렬이 함께 있을 때는 ESR rule을 반드시 따라야 합니다.
    • 범위 검색(range)을 정렬(sort) field 뒤에 배치하면, index의 정렬된 순서를 활용할 수 있습니다.
    • 만약 범위 검색이 정렬 field 앞에 오면, 범위 검색 이후 memory에서 정렬이 발생합니다.

Reference


목차