MongoDB populate vs aggregate 성능 비교 테스트

MongoDB populate vs aggregate 성능 비교 테스트

MongoDB를 사용할 때 populateaggregate는 자주 사용하는 두 가지 방식입니다. 이 글에서는 이 두 방법의 성능을 비교해 보겠습니다. 작은 데이터셋과 큰 데이터셋에서 각각의 성능을 테스트해 보았습니다.

테스트 환경

  • MongoDB 6.x.x
  • Node.js: 20.x.x
  • Mongoose: 8.x.x

Mongoose 모델 정의

먼저, 두 개의 Mongoose 모델을 정의했습니다: AuthorBook.

1
2
3
4
5
6
7
8
9
10
11
const mongoose = require("mongoose");
const { Schema } = mongoose;

const authorSchema = new Schema({ name: String });
const bookSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: "Author" },
});

const Author = mongoose.model("Author", authorSchema);
const Book = mongoose.model("Book", bookSchema);

테스트 데이터 생성

각 테스트 전 데이터를 새로 생성하여 캐싱 효과를 최소화했습니다. 10개의 문서작은 데이터셋을, 500,000개의 문서큰 데이터셋을 테스트했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function createTestData(numAuthors, numBooks) {
await Author.deleteMany({});
await Book.deleteMany({});

const authors = [];
for (let i = 0; i < numAuthors; i++) {
authors.push(new Author({ name: `Author ${i}` }));
}
await Author.insertMany(authors);

const books = [];
for (let i = 0; i < numBooks; i++) {
const author = authors[i % numAuthors];
books.push(new Book({ title: `Book ${i}`, author: author._id }));
}
await Book.insertMany(books);
}

성능 테스트

populateaggregate 각각의 성능을 테스트했습니다. 실행 시간은 performance.now()를 사용하여 측정했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
const { performance } = require("perf_hooks");

async function testPerformance() {
const smallTestSize = 10;
const largeTestSize = 500000;

// Small test
console.log(`Testing with ${smallTestSize} documents...`);

await createTestData(smallTestSize / 2, smallTestSize);

console.log(`Testing populate with ${smallTestSize} documents...`);
let startTime = performance.now();
await Book.find().populate("author").exec();
let endTime = performance.now();
const populateSmallTime = endTime - startTime;
console.log(
`populate with ${smallTestSize} documents: ${populateSmallTime} ms`
);

await createTestData(smallTestSize / 2, smallTestSize);

console.log(`Testing aggregate with ${smallTestSize} documents...`);
startTime = performance.now();
await Book.aggregate([
{
$lookup: {
from: "authors",
localField: "author",
foreignField: "_id",
as: "authorDetails",
},
},
{
$unwind: "$authorDetails",
},
]).exec();
endTime = performance.now();
const aggregateSmallTime = endTime - startTime;
console.log(
`aggregate with ${smallTestSize} documents: ${aggregateSmallTime} ms`
);

if (populateSmallTime < aggregateSmallTime) {
console.log(`populate is faster for ${smallTestSize} documents.`);
} else {
console.log(`aggregate is faster for ${smallTestSize} documents.`);
}

// Large test
console.log(`Testing with ${largeTestSize} documents...`);

await createTestData(largeTestSize / 2, largeTestSize);

console.log(`Testing populate with ${largeTestSize} documents...`);
startTime = performance.now();
await Book.find().populate("author").exec();
endTime = performance.now();
const populateLargeTime = endTime - startTime;
console.log(
`populate with ${largeTestSize} documents: ${populateLargeTime} ms`
);

await createTestData(largeTestSize / 2, largeTestSize);

console.log(`Testing aggregate with ${largeTestSize} documents...`);
startTime = performance.now();
await Book.aggregate([
{
$lookup: {
from: "authors",
localField: "author",
foreignField: "_id",
as: "authorDetails",
},
},
{
$unwind: "$authorDetails",
},
]).exec();
endTime = performance.now();
const aggregateLargeTime = endTime - startTime;
console.log(
`aggregate with ${largeTestSize} documents: ${aggregateLargeTime} ms`
);

if (populateLargeTime < aggregateLargeTime) {
console.log(`populate is faster for ${largeTestSize} documents.`);
} else {
console.log(`aggregate is faster for ${largeTestSize} documents.`);
}

mongoose.connection.close();
}

testPerformance().catch((err) => console.error(err));

테스트 결과

1
2
3
4
5
6
7
8
9
10
11
12
Testing with 10 documents...
Testing populate with 10 documents...
populate with 10 documents: 12.616999864578247 ms
Testing aggregate with 10 documents...
aggregate with 10 documents: 3.547708034515381 ms
aggregate is faster for 10 documents.
Testing with 500000 documents...
Testing populate with 500000 documents...
populate with 500000 documents: 6424.865000009537 ms
Testing aggregate with 500000 documents...
aggregate with 500000 documents: 10192.34625005722 ms
populate is faster for 500000 documents.
  1. 작은 데이터셋 (10 documents)
  • populate 실행 시간: 12.62 ms
  • aggregate 실행 시간: 3.55 ms
  • 결론: 작은 데이터셋에서는 aggregate가 더 빠르다.
  1. 큰 데이터셋 (500,000 documents)
  • populate 실행 시간: 6424.87 ms
  • aggregate 실행 시간: 10192.35 ms
  • 결론: 큰 데이터셋에서는 populate가 더 빠르다.

결론

위의 성능 테스트 결과를 토대로 아래와 같은 결론을 도출했습니다.

  • 작은 데이터셋에서는 aggregate가 더 빠르다.
  • 큰 데이터셋에서는 populate가 더 빠르다.

따라서, 데이터셋의 크기와 데이터베이스 구조를 고려하여 적절한 방법을 선택하는 것이 중요합니다.
이 블로그는 기술블로그마다 populate가 빠르다 또는 aggregate가 빠르다라는 게시글을 보고 의문이 들어서 직접 테스트해보았습니다.
MongoDB 버전이나 Mongoose 버전에 따라 결과가 달라질 수 있으니 참고하시기 바랍니다.
위 코드는 GitHub에 올려두었습니다. GitHub에서 확인하실 수 있습니다.

공유하기