database
MongoDB는 대용량 데이터를 효율적으로 관리하기 위해 다양한 집계(aggregation) 기능을 제공합니다. 그중에서도 count 집계 방식은 특정 조건에 맞는 도큐먼트의 개수를 빠르게 파악할 수 있는 중요한 기능입니다.
MongoDB 초기에는 단순하게 count() 메서드를 사용하여 조건에 맞는 도큐먼트의 개수를 조회했습니다. 예를 들어, status가 "active"인 도큐먼트의 개수를 확인하려면 다음과 같이 사용했습니다.
db.collection.count({ status: 'active' })
하지만 이 메서드는 몇 가지 한계를 가지고 있습니다. 복잡한 쿼리나 조인(join) 같은 경우에 정확한 결과를 보장하지 못할 수 있습니다. 매우 큰 컬렉션에서 count()를 사용하면 전체 스캔이 일어나 성능 저하가 발생할 수 있습니다. 따라서 따라서 MongoDB 4.0 이후에는 보다 명확한 의도를 가진 두 가지 메서드, 즉 countDocuments()와 estimatedDocumentCount()가 도입되었습니다.
countDocuments()는 지정한 쿼리 조건에 따라 정확하게 도큐먼트의 수를 계산합니다. status가 "active"인 도큐먼트를 정확하게 카운트할 때 사용합니다.
db.collection
.countDocuments({ status: 'active' })
.then((count) => {
console.log('Active documents:', count)
})
.catch((err) => {
console.error('Error:', err)
})
이 방식은 쿼리 필터를 적용한 후 실제로 도큐먼트를 스캔하여 개수를 계산하기 때문에, 데이터의 정확한 수를 확인할 수 있습니다. 단, 전체 스캔이 발생할 수 있으므로 성능에 민감한 경우 주의해야 합니다.
estimatedDocumentCount()는 컬렉션의 메타데이터를 기반으로 전체 도큐먼트 수를 빠르게 추정합니다. 특정 조건 없이 전체 컬렉션의 대략적인 도큐먼트 수를 빠르게 파악할 때 유용합니다.
db.collection
.estimatedDocumentCount()
.then((count) => {
console.log('Estimated total documents:', count)
})
.catch((err) => {
console.error('Error:', err)
})
이 방법은 빠르게 결과를 얻을 수 있지만, 정확한 값이 필요한 경우에는 적합하지 않을 수 있습니다.
MongoDB의 집계 파이프라인을 사용하면, 보다 유연하게 도큐먼트를 그룹화하거나 조건을 적용한 후, 개수를 계산할 수 있습니다. 특히 $count 스테이지를 활용하면, 파이프라인의 마지막에 집계된 결과의 개수를 출력할 수 있습니다. 예를 들어, status가 "active"인 도큐먼트를 집계 파이프라인으로 카운트하려면 아래와 같이 사용할 수 있습니다.
db.collection
.aggregate([{ $match: { status: 'active' } }, { $count: 'activeCount' }])
.toArray()
.then((results) => {
console.log('Aggregation count result:', results)
})
.catch((err) => {
console.error('Error:', err)
})
이 방식의 장점은, 여러 단계의 집계 연산을 결합할 수 있다는 점입니다. 예를 들어, 먼저 조건을 적용하고, 그 결과를 그룹화한 후, 각 그룹의 도큐먼트 수를 계산하는 복합적인 작업을 수행할 수 있습니다. 또한 $group과 $sum을 조합하여 더 복잡한 카운팅 로직을 구현할 수도 있습니다.
정리하면, 정확한 조건에 따른 도큐먼트 개수가 필요하다면, countDocuments()를 사용하는 것이 좋고 전체 컬렉션의 빠른 추정치가 필요하다면, estimatedDocumentCount()가 유용합니다. 복잡한 집계 연산과 함께 도큐먼트 수를 계산해야 한다면, 집계 파이프라인의 $count 스테이지를 활용할 수 있습니다.
다양한 예를 통해 조금 더 이해도를 높여 봅시다.
// countDocuments 사용
db.collection
.countDocuments({ status: 'active' })
.then((count) => console.log('Active documents:', count))
.catch((err) => console.error('Error:', err))
// 집계 파이프라인 사용
db.collection
.aggregate([{ $match: { status: 'active' } }, { $count: 'activeCount' }])
.toArray()
.then((results) => console.log('Aggregation count result:', results))
.catch((err) => console.error('Error:', err))
실무 환경에서는 단순한 조건 집계 외에도 여러 단계의 연산을 결합해 복잡한 분석을 수행하는 경우가 많습니다. 예를 들어, 전자상거래 데이터베이스에서 고객별 주문 건수와 총 구매 금액을 집계하고, 일정 주문 건수 이상인 고객만을 추려내어 상위 10명의 고객을 확인하는 상황을 고려해 봅시다.
우리는 "배송 완료(delivered)" 또는 "배송 중(shipped)" 상태인 주문만을 대상으로, 고객별 주문 건수와 구매 총액을 집계한 후, 주문 건수가 5건을 초과하는 고객들 중 구매 총액이 높은 순서대로 상위 10명을 추출해보겠습니다.
db.orders
.aggregate([
// 1. 배송 완료(delivered) 또는 배송 중(shipped)인 주문만 필터링
{ $match: { status: { $in: ['delivered', 'shipped'] } } },
// 2. 고객별로 그룹화하여 주문 건수와 구매 총액 계산
{
$group: {
_id: '$customerId',
orderCount: { $sum: 1 },
totalSpent: { $sum: '$totalAmount' },
},
},
// 3. 주문 건수가 5건을 초과하는 고객만 필터링
{ $match: { orderCount: { $gt: 5 } } },
// 4. 구매 총액을 기준으로 내림차순 정렬
{ $sort: { totalSpent: -1 } },
// 5. 상위 10명의 고객만 선택
{ $limit: 10 },
])
.toArray()
.then((results) => {
console.log('Top 10 customers:', results)
})
.catch((err) => {
console.error('Aggregation error:', err)
})
이 예제에서는 다음과 같은 단계가 수행됩니다.
SAMPLE. 2에서 추가 요구사항을 구현해 보겠습니다. 한 번의 집계 파이프라인 내에서 상위 10명의 고객과 전체 조건을 만족하는 고객 수를 동시에 구할 수 있습니다. 이는 네트워크 왕복을 한 번으로 줄여 성능 상 이점을 제공할 수 있지만, 파이프라인이 복잡해지면 처리 비용이 증가할 수 있습니다.
db.orders
.aggregate([
{ $match: { status: { $in: ['delivered', 'shipped'] } } },
{
$group: {
_id: '$customerId',
orderCount: { $sum: 1 },
totalSpent: { $sum: '$totalAmount' },
},
},
{
$facet: {
topCustomers: [
{ $match: { orderCount: { $gt: 5 } } },
{ $sort: { totalSpent: -1 } },
{ $limit: 10 },
],
totalCustomers: [{ $count: 'totalCount' }],
},
},
])
.toArray()
.then((results) => {
console.log('Aggregation result:', results)
})
.catch((err) => console.error('Aggregation error:', err))
이 방식은 한 번의 집계로 상위 10명과 전체 조건을 만족하는 고객 수를 모두 반환합니다. 결론적으로, 데이터 양과 쿼리 복잡도에 따라 한 번의 복합 집계 파이프라인(예: $facet 사용)과 별도의 쿼리를 실행하는 방식 중 적절한 방법을 선택하면 됩니다. 상황에 따라 성능 테스트를 통해 최적의 방식을 결정하는 것이 좋습니다. 따라서 데이터 양이 많고 복잡한 집계 연산이 있다면, 전체 개수를 구하는 쿼리를 따로 실행하여 결과를 결합하는 것이 더 나은 선택일 수 있습니다.
아래 예시는 $facet를 사용하지 않고, 두 개의 별도 집계 쿼리를 병렬적으로 실행한 후 애플리케이션 레벨에서 결과를 결합하는 방식입니다. 이 예제는 Node.js 환경에서 async/await를 활용하여, 조건에 맞는 상위 10명의 고객 결과와 전체 조건을 만족하는 고객의 수를 동시에 조회하는 방법을 보여줍니다.
async function getTopCustomersAndTotalCount(db) {
// 상위 10명의 고객을 조회하는 파이프라인
const topCustomersPipeline = [
{ $match: { status: { $in: ['delivered', 'shipped'] } } },
{
$group: {
_id: '$customerId',
orderCount: { $sum: 1 },
totalSpent: { $sum: '$totalAmount' },
},
},
{ $match: { orderCount: { $gt: 5 } } },
{ $sort: { totalSpent: -1 } },
{ $limit: 10 },
]
// 전체 조건을 만족하는 고객 수를 계산하는 파이프라인
// 먼저 고객별로 그룹화한 후 조건을 적용하고, 마지막에 $count로 전체 개수를 계산
const totalCountPipeline = [
{ $match: { status: { $in: ['delivered', 'shipped'] } } },
{
$group: {
_id: '$customerId',
orderCount: { $sum: 1 },
},
},
{ $match: { orderCount: { $gt: 5 } } },
{ $count: 'totalCount' },
]
// 두 개의 집계 쿼리를 병렬로 실행
const [topCustomers, totalCountResult] = await Promise.all([
db.collection('orders').aggregate(topCustomersPipeline).toArray(),
db.collection('orders').aggregate(totalCountPipeline).toArray(),
])
// totalCountResult는 배열로 반환되므로, 값이 존재하면 추출, 없으면 0으로 처리
const totalCount =
totalCountResult.length > 0 ? totalCountResult[0].totalCount : 0
return { topCustomers, totalCount }
}
// 사용 예시:
getTopCustomersAndTotalCount(db)
.then(({ topCustomers, totalCount }) => {
console.log('Top 10 customers:', topCustomers)
console.log('Total matching customers:', totalCount)
})
.catch((err) => {
console.error('Aggregation error:', err)
})
위 코드는 두 가지 별도의 집계 파이프라인을 동시에 실행하여, 상위 10명의 고객을 조회하는 파이프라인과, 전체 조건을 만족하는 고객 수를 계산하는 파이프라인을 각각 실행합니다. Promise.all을 사용하여 두 쿼리를 병렬 처리함으로써, 네트워크 왕복 횟수를 줄이고 전체 처리 시간을 최적화할 수 있습니다. 최종적으로, 상위 10명의 고객 결과와 전체 고객 수를 결합하여 반환합니다. 이와 같이, $facet를 사용하지 않고 별도의 집계 쿼리를 결합하는 방식도 충분히 유용하며, 데이터 양이 많거나 복잡한 집계 연산의 경우 각각의 쿼리를 최적화하여 독립적으로 관리할 수 있는 장점이 있습니다.
작은 규모의 데이터셋이나 집계가 자주 실행되지 않는 경우에는 $facet 방식이 매우 간편하고 효과적일 수 있습니다. 그러나 대규모 데이터셋이나 성능 민감도가 높은 환경에서는, 전체 개수를 별도의 간단한 쿼리로 조회한 후 상위 10명의 결과와 결합하는 방식이 더 효율적일 수도 있습니다. 결론적으로, 위 방법은 상황에 맞는 하나의 옵션일 뿐, 특정 시나리오에서는 별도의 쿼리 방식이 더 나을 수도 있습니다.