2026년 1월 21일 작성

ESR Rule - Compound Index 설계의 핵심 원칙

ESR Rule은 Equality, Sort, Range 순서로 compound index의 field를 배치하는 원칙으로, query 성능을 획기적으로 향상시킵니다.

ESR Rule : Compound Index 생성 시 지켜야 할 Field 순서 설계 원칙

“Design your compound indexes following the ESR rule: Equality fields first, followed by Sort fields, and finally Range fields.”

  • ESR Rule은 compound index를 생성할 때 field 순서를 결정하는 설계 원칙입니다.
    • createIndex({ Equality, Sort, Range }) 순서로 field를 배치합니다.
    • query 작성 시 field 순서는 상관없으며, index 생성 시 이 순서가 성능을 결정합니다.
  • ESR Rule에 따라 index를 생성하면 filtering과 정렬을 모두 index에서 처리합니다.
    • memory에서의 정렬이 발생하지 않습니다.
    • document 접근을 최소화합니다.
    • MongoDB query optimizer가 해당 index를 최대한 효율적으로 활용합니다.
  • 같은 field로 만든 index라도 field 순서에 따라 성능이 극적으로 달라집니다.
    • 올바른 순서 : 약 25ms 실행 시간.
    • 잘못된 순서 : 약 2,500ms 실행 시간 (약 100배 차이).
    • MongoDB 공식 문서에서 권장하는 best practice입니다.

3가지 Component : Equality, Sort, Range

  • Equality : 정확한 값으로 filtering하는 field입니다.
    • { status: "active" } 같은 조건의 field입니다.
    • index에서 후보를 빠르게 좁혀줍니다.
  • Sort : query 결과를 정렬하는 field입니다.
    • sort({ createdAt: -1 }) 같은 정렬 조건의 field입니다.
    • index에 이미 정렬된 순서로 배치되므로, 추가 정렬이 불필요합니다.
  • Range : 범위 조건으로 filtering하는 field입니다.
    • { age: { $gte: 20 } } 같은 범위 조건의 field입니다.
    • index의 마지막에 배치하면 memory sort를 피할 수 있습니다.

Compound Index에서만 ESR Rule이 중요한 이유

  • ESR Rule은 여러 field의 순서를 정의하는 원칙이므로, field가 하나인 single index에는 적용 불가능합니다.
    • single index : db.users.createIndex({ status: 1 }) → status만 색인하므로 field 순서 개념이 없음.
    • compound index : db.users.createIndex({ status: 1, createdAt: -1, age: 1 }) → 3개 field의 순서가 성능을 결정함.
  • compound index에서 field 순서가 성능을 극적으로 결정합니다.
    • 같은 3개 field라도 순서가 다르면 성능이 완전히 달라집니다.
    • ESR rule을 무시하면 memory sort, index intersection 등으로 인해 query 성능이 100배 이상 저하될 수 있습니다.
  • 여러 single index보다 하나의 compound index가 훨씬 효율적입니다.
    • single index만 있으면 MongoDB는 여러 index를 조합(index intersection)해야 하는데 복잡하고 비효율적입니다.
    • ESR rule을 따른 하나의 compound index로 모든 조건을 효율적으로 처리할 수 있습니다.
  • filtering, sorting, range 조건이 섞여 있을 때 compound index의 가치가 극대화됩니다.
    • 조건이 하나만 있으면 single index로도 충분합니다.
    • 여러 조건이 함께 있을 때 compound index의 field 순서가 매우 중요합니다.

ESR Rule 적용 예시

  • ESR Rule은 query pattern에 따라 모든 component를 적용하거나 일부만 적용할 수 있습니다.
    • Equality, Sort, Range가 모두 있을 때 : 완전한 ESR rule 적용.
    • 일부 component만 있을 때 : 해당 부분만 적용하며, 기본 원칙(filtering field → sort field → range field 순서)은 동일.

Equality + Sort + Range가 모두 있을 때

// query
db.users.find({
    status: "active",                    // Equality
    age: { $gte: 25 }                    // Range
}).sort({ createdAt: -1 });              // Sort

// 잘못된 설계 (field 순서 부적절)
db.users.createIndex({ age: 1, status: 1, createdAt: -1 });
// age 범위 검색 후 memory에서 정렬 필요

// 올바른 설계 (완전한 ESR rule)
db.users.createIndex({ status: 1, createdAt: -1, age: 1 });
// status로 filtering → createdAt으로 정렬 (index 활용) → age 범위 (index 활용)
  • ESR rule을 따르면 모든 작업이 index에서 처리됩니다.

여러 Equality field가 있을 때

// query
db.orders.find({
    customerId: "123",                   // Equality
    status: "completed",                 // Equality
    amount: { $gte: 100 }                // Range
}).sort({ orderDate: -1 });              // Sort

// 잘못된 설계
db.orders.createIndex({ amount: 1, orderDate: 1, customerId: 1, status: 1 });

// 올바른 설계 (완전한 ESR rule)
db.orders.createIndex({ customerId: 1, status: 1, orderDate: -1, amount: 1 });
// customerId와 status로 filtering → orderDate로 정렬 → amount 범위
  • 여러 equality field가 있을 때도 같은 원칙을 적용합니다.
  • selectivity가 높은 field를 먼저 배치하면 더 효율적입니다.

Equality + Range만 있을 때 (정렬 없음)

// query
db.products.find({
    category: "electronics",             // Equality
    price: { $lt: 1000 }                 // Range
});

// 설계 (E-R rule 부분 적용)
db.products.createIndex({ category: 1, price: 1 });
// category로 filtering → price 범위 (E-R)
  • Sort component가 없으므로 E-R 순서로만 배치합니다.

Equality + Sort만 있을 때 (범위 검색 없음)

// query
db.articles.find({
    author: "John"                       // Equality
}).sort({ views: -1 });                  // Sort

// 설계 (E-S rule 부분 적용)
db.articles.createIndex({ author: 1, views: -1 });
// author로 filtering → views 정렬 (E-S)
  • Range component가 없으므로 E-S 순서로만 배치합니다.

실전 사례

  • e-commerce 상품 검색, 사용자 활동 분석, 로그 검색 등 실무 상황에서 ESR rule을 적용하여 index를 설계할 수 있습니다.

e-commerce 상품 검색

  • 여러 filtering 조건과 정렬이 함께 있는 전형적인 검색 query입니다.
// query
db.products.find({
    status: "active",                    // Equality
    category: "electronics",             // Equality
    price: { $gte: 100, $lte: 1000 }    // Range
}).sort({ rating: -1 }).limit(20);      // Sort

// 설계 (완전한 ESR rule)
db.products.createIndex({
    status: 1,        // E1
    category: 1,      // E2 (selectivity 고려)
    rating: -1,       // S
    price: 1          // R
});

// 실행 계획 : status filtering → category filtering → rating 정렬 (index 활용) → price 범위

사용자 활동 분석

  • 기간 범위와 정렬 조건이 있는 분석 query의 예시입니다.
// query
db.userActivity.find({
    userId: "user123",                   // Equality
    eventType: "purchase",               // Equality
    timestamp: {                         // Range
        $gte: new Date("2025-01-01"),
        $lte: new Date("2025-12-31")
    }
}).sort({ amount: -1 }).limit(100);     // Sort

// 설계 (완전한 ESR rule)
db.userActivity.createIndex({
    userId: 1,        // E
    eventType: 1,     // E
    amount: -1,       // S
    timestamp: 1      // R
});

Log 검색

  • 정렬 field가 range 조건과 겹치는 경우로, ESR rule을 부분적으로 적용합니다.
// query
db.logs.find({
    level: "error",                      // Equality
    service: "payment",                  // Equality
    timestamp: { $gte: Date.now() - 86400000 }  // Range (최근 24시간)
}).sort({ timestamp: -1 }).limit(50);   // Sort

// 설계 (부분 적용: timestamp가 Sort에 포함)
db.logs.createIndex({
    level: 1,         // E
    service: 1,       // E
    timestamp: -1,    // S
});

// 참고 : timestamp가 이미 Sort에 포함되어 있으므로 Range로 따로 지정할 필요 없음

ESR Rule이 중요한 이유

  • index field 순서를 잘못 설계하면 memory sort, index intersection 등으로 query 성능이 100배 이상 저하될 수 있습니다.

Memory Sort의 성능 저하

// query
db.users.find({
    status: "active",
    age: { $gte: 20 }
}).sort({ createdAt: -1 });

// ESR rule 미적용 : { age: 1, status: 1, createdAt: -1 }
// 1. age 범위 검색 : 50,000개 document 검사
// 2. memory sort : 50,000개를 createdAt으로 정렬
// 3. memory 제한 : 104MB 이상 정렬 시 실패 가능
{
    "nReturned": 100,
    "totalDocsExamined": 50000,
    "executionTimeMillis": 2500
}

// ESR rule 적용 : { status: 1, createdAt: -1, age: 1 }
// 1. status filtering : 5,000개로 축소
// 2. createdAt 정렬 : index에 이미 정렬됨 (추가 작업 없음)
// 3. age 범위 : 100개만 반환
{
    "nReturned": 100,
    "totalDocsExamined": 100,
    "executionTimeMillis": 25
}
  • Memory sort는 성능을 100배 이상 저하시킵니다.

Index Intersection 회피

// ESR rule 미적용 시 MongoDB의 상황
db.users.createIndex({ age: 1 });
db.users.createIndex({ status: 1 });
db.users.createIndex({ createdAt: -1 });

db.users.find({
    status: "active",
    age: { $gte: 20 }
}).sort({ createdAt: -1 });

// MongoDB가 여러 index를 조합하려 시도 → 복잡하고 비효율적
// 또는 하나의 index만 선택 후 나머지는 memory에서 처리
  • ESR rule을 따르는 하나의 compound index가 여러 단일 index보다 훨씬 효율적입니다.

Working Set 최적화

  • working set은 query 처리에 필요한 모든 data(index + document)입니다.
  • ESR rule 적용으로 검사 document 수가 줄어들면, 더 많은 index가 memory에 올라가고 cache hit rate가 증가하여 query 성능이 향상됩니다.
flowchart LR
    A[ESR rule 적용] --> B[검사 document 수 감소]
    B --> C[working set 축소]
    C --> D[더 많은 index가 memory에 상주]
    D --> E[cache hit rate 증가]
    E --> F[query 성능 향상]

ESR Rule 적용 시 고려 사항

  • query pattern, selectivity, write performance 등을 고려하여 실무 상황에 맞게 ESR rule을 적용할 수 있습니다.

Query Pattern 분석의 중요성

// query pattern 1 : status로 많이 filtering
db.users.find({ status: "active" }).sort({ createdAt: -1 });

// query pattern 2 : age 범위로 많이 검색
db.users.find({ age: { $gte: 20 } }).sort({ createdAt: -1 });

// 가장 자주 사용되는 query를 기준으로 index 설계
// 1번 pattern이 더 많으면 : { status: 1, createdAt: -1, age: 1 }
// 2번 pattern이 더 많으면 : age는 range이므로 ESR rule 위반
  • 가장 자주 실행되는 query를 기준으로 ESR rule을 적용해야 합니다.
  • 모든 query를 100% 최적화할 수는 없습니다.

Selectivity 고려

// 높은 selectivity : status 값이 적음 (10개 정도)
// 낮은 selectivity : age 범위가 너무 넓음 (전체 범위)

// 이 경우 더 selective한 field를 먼저 배치
db.users.createIndex({ status: 1, createdAt: -1, age: 1 });
// status로 빠르게 축소 → createdAt 정렬 → age 범위
  • Equality field 여럿이 있을 때 더 selective한 field를 앞에 배치하면 더 효율적입니다.

Write Performance Trade-off

// ESR rule을 따르는 여러 compound index 생성
db.users.createIndex({ status: 1, createdAt: -1, age: 1 });
db.users.createIndex({ department: 1, createdAt: -1, salary: 1 });
db.users.createIndex({ region: 1, createdAt: -1, experience: 1 });

// document 삽입 시 모든 index를 update해야 함
db.users.insertOne({ ... }); // 3개 index 모두 update
  • ESR rule을 정확히 적용한 index는 query 성능은 최적이지만 write 성능은 저하됩니다.
  • 필요한 query만 선택적으로 index를 만들어야 합니다.

Sort 방향의 영향

// query 1
db.products.find({ category: "electronics" })
    .sort({ price: 1 });  // ascending

// query 2
db.products.find({ category: "electronics" })
    .sort({ price: -1 }); // descending

// 두 query를 모두 index에서 처리하려면
db.products.createIndex({ category: 1, price: 1 });
// query 1은 최적 (ascending 일치)
// query 2는 backward scan 필요 (약간의 성능 저하)
  • 정렬 방향도 중요하지만, ascending과 descending 모두 효율적으로 처리할 수 있습니다.
  • 완벽히 일치하지 않아도 memory sort보다 훨씬 효율적입니다.

ESR Rule 적용 시 확인 사항

  • index 설계 전에는 query pattern을 파악하고, 설계 중에는 field 순서를 확인하며, 적용 후에는 explain()으로 실행 계획을 검증해야 합니다.

Index 설계 전

  • 자주 실행되는 query들을 파악했는가.
  • 각 query의 filtering 조건(equality, range)을 분류했는가.
  • 각 query의 정렬(sort) 조건을 파악했는가.
  • 가장 중요한 query를 선정했는가.

Index 설계 중

  • equality field들을 앞에 배치했는가.
  • sort field를 그 다음에 배치했는가.
  • range field를 마지막에 배치했는가.
  • 더 selective한 equality field가 앞에 있는가.

Index 적용 후

  • explain("executionStats")로 실행 계획을 확인했는가.
  • SORT stage가 없는가.
  • nReturnedtotalDocsExamined의 비율이 1:1에 가까운가.
  • 실행 시간이 100ms 이하인가.

Reference


목차