performance2025-02-24·8 min·174/348

MongoDB count() 성능 문제 해결: 효율적인 카운팅 전략

MongoDB count() 함수의 성능 문제를 분석하고 대체 방법을 통한 최적화 전략을 설명합니다.

MongoDB count() 성능 문제 해결

Introduction

MongoDB에서 count() 함수는 컬렉션의 문서 수를 반환하는 가장 기본적인 작업이지만, 대용량 컬렉션에서는 심각한 성능 문제를 일으킬 수 있습니다. 특히 count() without query는 전체 컬렉션을 스캔하여 느린 실행 시간을 초래합니다. 이 글에서는 count()의 내부 동작과 최적화 방법을 다루겠습니다.

Environment

// MongoDB 연결 설정
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('mydb');
const collection = db.collection('users');

// 테스트 데이터 생성
async function createTestData() {
  const bulkOps = [];
  for (let i = 0; i < 5000000; i++) {
    bulkOps.push({
      insertOne: {
        document: {
          name: `user_${i}`,
          email: `user${i}@example.com`,
          age: Math.floor(Math.random() * 60) + 18,
          status: ['active', 'inactive', 'pending'][Math.floor(Math.random() * 3)],
          createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000)
        }
      }
    });
  }
  await collection.bulkWrite(bulkOps);
}

Problem

count() 함수의 성능 문제:

// 전체 문서 수 카운팅 - 매우 느림
console.time('count');
const totalCount = await collection.count();
console.timeEnd('count');
// count: 45234ms (45초!)

// 조건부 카운팅 - 여전히 느림
console.time('countWithQuery');
const activeCount = await collection.count({ status: 'active' });
console.timeEnd('countWithQuery');
// countWithQuery: 38901ms
// explain으로 성능 분석
const explainResult = await collection.find({ status: 'active' }).explain('executionStats');
console.log(JSON.stringify(explainResult, null, 2));

// 결과:
// {
//   "executionStats": {
//     "executionTimeMillis": 38901,
//     "totalDocsExamined": 5000000,  // 전체 문서 검사!
//     "totalKeysExamined": 0,         // 인덱스 미사용
//     "executionStages": {
//       "stage": "COLLSCAN",          // 컬렉션 스캔
//       "nReturned": 1666667
//     }
//   }
// }

Analysis

count() 성능 문제의 원인을 분석했습니다:

// MongoDB count() 동작 원리
// 1. count() without query: 전체 컬렉션 스캔
// 2. count(query): 쿼리 조건에 맞는 문서 스캔
// 3. InnoDB 스토리지 엔진에서는 정확한 수를 반환하기 위해 스캔 필요

// 컬렉션 통계 확인
const stats = await db.command({ collStats: 'users' });
console.log('컬렉션 통계:', {
  count: stats.count,
  size: stats.size,
  storageSize: stats.storageSize,
  totalIndexSize: stats.totalIndexSize
});

// 인덱스 상태 확인
const indexes = await collection.indexes();
console.log('인덱스:', indexes);
// [{ _id: 1 }]  // 기본 인덱스만 존재

Solution

1단계: estimatedDocumentCount 사용

// 전체 문서 수: estimatedDocumentCount 사용
console.time('estimatedCount');
const estimatedCount = await collection.estimatedDocumentCount();
console.timeEnd('estimatedCount');
// estimatedCount: 12ms (12밀리초!)

console.log('추정 문서 수:', estimatedCount);
// 추정 문서 수: 4998234

// estimatedDocumentCount는 카탈로그 메타데이터를 사용하여
// 빠르게 추정치를 반환

2단계: 집계 파이프라인 활용

// 조건부 카운팅: $count 활용
console.time('aggregationCount');
const result = await collection.aggregate([
  { $match: { status: 'active' } },
  { $count: 'total' }
]).toArray();
console.timeEnd('aggregationCount');
// aggregationCount: 892ms

console.log('활성 사용자 수:', result[0].total);

// 복합 조건 카운팅
const complexResult = await collection.aggregate([
  { 
    $match: { 
      status: 'active',
      age: { $gte: 20, $lte: 30 },
      createdAt: { 
        $gte: new Date('2024-01-01'),
        $lt: new Date('2025-01-01')
      }
    } 
  },
  { $count: 'total' }
]).toArray();

console.log('복합 조건 결과:', complexResult[0].total);

3단계: 인덱스 최적화

// 쿼리 패턴에 맞는 인덱스 생성
await collection.createIndex({ status: 1 });
await collection.createIndex({ age: 1 });
await collection.createIndex({ createdAt: -1 });
await collection.createIndex({ status: 1, age: 1 });

// 인덱스 사용 확인
const explainResult = await collection.find({ status: 'active' }).explain('executionStats');
console.log('인덱스 사용:', explainResult.executionStats.totalKeysExamined > 0);
// 인덱스 사용: true

// 실행 시간 비교
console.time('countWithIndex');
const count = await collection.count({ status: 'active' });
console.timeEnd('countWithIndex');
// countWithIndex: 45ms (기존 대비 860배 향상!)

4단계: 캐싱 전략

// 카운트 결과 캐싱
const NodeCache = require('node-cache');
const countCache = new NodeCache({ stdTTL: 60 });  // 60초 캐시

async function getCachedCount(query) {
  const cacheKey = JSON.stringify(query);
  
  let count = countCache.get(cacheKey);
  if (count !== undefined) {
    return count;
  }
  
  // 캐시 미스 시 DB에서 조회
  if (Object.keys(query).length === 0) {
    count = await collection.estimatedDocumentCount();
  } else {
    const result = await collection.aggregate([
      { $match: query },
      { $count: 'total' }
    ]).toArray();
    count = result[0]?.total || 0;
  }
  
  countCache.set(cacheKey, count);
  return count;
}

// 사용 예
const activeCount = await getCachedCount({ status: 'active' });

Lessons Learned

  1. estimatedDocumentCount 활용: 전체 문서 수가 필요하면 반드시 estimatedDocumentCount()를 사용해야 합니다
  2. $count 파이프라인: 조건부 카운팅은 집계 파이프라인의 $count 단계를 사용하는 것이 효율적입니다
  3. 인덱스 필수: 카운팅 쿼리에 사용되는 필드에 인덱스를 생성해야 합니다
  4. 캐싱 전략: 빈번한 카운팅 요청은 캐싱을 통해 처리량을 향상시켜야 합니다
  5. 정확성 vs 성능: estimatedDocumentCount()는 추정치를 반환하므로 정확한 수가 필요하면 countDocuments()를 사용합니다

이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.