260215 LiteDB 1000건 페이징 순회 성능예측
15 Feb 2026
⚡ 260215 LiteDB 1000건 단위 페이징 순회 로딩 성능 예측
💡 이전 문서 LiteDB byte[] 변환 시 Full Query 성능 변화의 후속 리서치로, 50만건 MyMesh를 1,000건 단위로 페이징 순회할 때의 수행시간과 메모리 사용량을 예측한다.
📌 1. 전제 조건
| 항목 | 값 |
|---|---|
| 테이블 | MyMesh |
| 총 행 수 | 500,000건 |
| 정점 수/건 | 1,000개 (vertexList: float×3,000, indexList: int×1,000, triIndexList: int×3,000) |
| 저장 방식 | byte[] 변환 저장 (BsonBinary) |
| 문서 크기 | ~28 KB/건 (byte[] 변환 후) |
| 배치 크기 | 1,000건 |
| 배치 수 | 500회 |
| DB 파일 크기 | ~13 GB |
| 환경 | Windows 11, Unity, IL2CPP/Mono, SSD |
⚡ 2. 페이징 방식별 성능 차이: Skip/Offset vs Keyset
LiteDB에서 페이징을 구현하는 방법은 크게 두 가지다. 어떤 방식을 선택하느냐에 따라 성능이 252배 차이난다.
🔹 2.1 Skip/Offset 방식 (느림)
// 전통적 Skip/Offset 방식
for (int skip = 0; skip < 500_000; skip += 1000)
{
var batch = col.Find(Query.All(), skip, 1000).ToList();
ProcessBatch(batch);
}
LiteDB의 Skip은 인덱스를 건너뛰지 않고 실제 문서를 순차 읽기하며 버린다. 즉 O(skip + limit) 비용이 발생한다.
배치 1: skip=0 → 0 + 1,000 = 1,000 문서 순회
배치 2: skip=1000 → 1,000 + 1,000 = 2,000 문서 순회
배치 3: skip=2000 → 2,000 + 1,000 = 3,000 문서 순회
...
배치 250: skip=249000 → 249,000 + 1,000 = 250,000 문서 순회
...
배치 500: skip=499000 → 499,000 + 1,000 = 500,000 문서 순회
총 순회량 = Σ(k=0→499) (k×1000 + 1000)
= 1000 × Σ(k=0→499)(k+1)
= 1000 × (500 × 501 / 2)
= 125,250,000 회 (약 1.25억 회)
💡 실제 데이터 50만건에 대해 1.25억 번 순회 — 250배 중복 읽기
🔹 2.2 Keyset Pagination 방식 (빠름, 권장)
// Keyset(커서) 방식 - Id 기반 범위 쿼리
long lastId = 0;
while (true)
{
var batch = col.Find(x => x.Id > lastId, limit: 1000).ToList();
if (batch.Count == 0) break;
ProcessBatch(batch);
lastId = batch[^1].Id; // 마지막 Id 기억
}
_id 인덱스를 통해 정확한 위치로 점프 후 1,000건만 읽는다. 배치마다 O(log n + limit) 비용.
배치 1: Id>0 → 19 step(인덱스) + 1,000 문서 = 1,019 순회
배치 2: Id>1000 → 19 step(인덱스) + 1,000 문서 = 1,019 순회
...
배치 500: Id>499000 → 19 step(인덱스) + 1,000 문서 = 1,019 순회
총 순회량 = 500 × 1,019 = 509,500 회 (약 51만 회)
💡 50만건에 대해 51만번 순회 — 거의 1:1
⚖️ 2.3 비교
| 항목 | Skip/Offset | Keyset (Id 범위) | 차이 |
|---|---|---|---|
| 총 순회량 | 1.25억 회 | 51만 회 | 252배 |
| 첫 번째 배치 | 1,000건 읽기 | 1,019건 읽기 | 동일 |
| 마지막 배치 | 500,000건 읽기 | 1,019건 읽기 | 491배 |
| 배치당 시간 (일정?) | 점점 느려짐 | 일정 |
📌 3. 수행 시간 예측
🗂️ 3.1 문서당 읽기 비용 (byte[] 변환 저장, ~28KB/건)
이전 문서에서 분석한 BsonBinary 역직렬화 비용:
| 단계 | 비용 |
|---|---|
| 인덱스 탐색 (Skip List) | ~0.3 μs/step |
| 페이지 읽기 (캐시 히트) | ~1~2 μs |
| 페이지 읽기 (디스크) | ~50~100 μs |
| BSON 역직렬화 (28KB, BsonBinary) | ~35~70 μs |
| 문서 1건 총 비용 | ~40~80 μs (캐시 히트 시) |
🔹 3.2 Skip/Offset 방식 예상 시간
배치별 소요시간:
배치 1 (skip=0): 1,000 × 60μs = ~60ms
배치 50 (skip=49000): 50,000 × 60μs = ~3초
배치 100 (skip=99000): 100,000 × 60μs = ~6초
배치 250 (skip=249000): 250,000 × 60μs = ~15초
배치 500 (skip=499000): 500,000 × 60μs = ~30초
총 소요시간 = Σ(평균 250,000건 × 60μs × 500배치의 가중평균)
≈ 1.25억 × 60μs
≈ 7,500초
≈ ~125분 (~2시간)
그러나 LiteDB 캐시 효과와 SSD 순차 읽기 최적화를 고려하면, skip된 문서는 역직렬화 없이 인덱스만 순회하므로 실제로는 더 빠르다.
| 시나리오 | 예상 총 시간 | 근거 |
|---|---|---|
| 최적 (캐시 히트, skip 경량) | ~15~25분 | skip이 인덱스만 순회한다고 가정 |
| 현실적 | ~20~40분 | 일부 디스크 I/O + 캐시 미스 |
| 최악 (모든 skip이 역직렬화) | ~2시간+ | 모든 건을 완전 역직렬화 |
🔹 3.3 Keyset Pagination 예상 시간
배치별 소요시간 (일정):
매 배치: 19 step(인덱스) + 1,000건 × 60μs = ~60ms
총 소요시간 = 500배치 × 60ms = 30,000ms = ~30초
| 시나리오 | 예상 총 시간 | 근거 |
|---|---|---|
| 최적 (캐시 히트) | ~30초 | 모든 페이지가 캐시에 있는 경우 |
| 현실적 | ~1~2분 | 디스크 I/O 포함 (13GB 순차 읽기 ~26초) |
| 최악 (캐시 미스 빈번) | ~3~5분 | SSD 랜덤 I/O 포함 |
⚖️ 3.4 비교표
| 항목 | Skip/Offset | Keyset | 개선율 |
|---|---|---|---|
| 현실적 총 시간 | ~20~40분 | ~1~2분 | ~20x |
| 첫 번째 배치 | ~60ms | ~60ms | 동일 |
| 마지막 배치 | ~30초 | ~60ms | ~500x |
| 시간 안정성 | 배치마다 증가 | 일정 |
📌 4. 메모리 사용량 예측
🔹 4.1 배치당 메모리 (1,000건 × 28KB, BsonBinary)
[사용자 데이터]
byte[] 배열 (vertex 12KB + index 4KB + tri 12KB) × 1000건
= 28,000 KB = ~27.3 MB
float[]/int[] 복원 배열 (vertex 12KB + index 4KB + tri 12KB) × 1000건
= 28,000 KB = ~27.3 MB
MyMesh 객체 오버헤드 (참조, 헤더 등)
= ~0.5 MB
List<MyMesh> 컨테이너
= ~0.01 MB
─────────────────────────────
사용자 데이터 합계: ~55 MB
[LiteDB 내부]
MemoryCache PageBuffer (8KB × ~3,500페이지)
= ~27.3 MB (배치당, 캐시 히트 시 재사용)
BSON 역직렬화 중간 객체
= ~5~10 MB (역직렬화 후 GC 대상)
─────────────────────────────
LiteDB 내부 합계: ~32~37 MB
[배치당 피크 메모리] = ~87~92 MB
🔹 4.2 순회 전체에 걸친 메모리 추이
핵심 문제: LiteDB v5의 MemoryCache는 캐시 상한이 없어, 순회할수록 캐시가 누적된다.
배치 1: 피크 ~90MB, LiteDB 캐시 ~27MB
배치 10: 피크 ~90MB, LiteDB 캐시 ~100MB (캐시 누적)
배치 50: 피크 ~90MB, LiteDB 캐시 ~300MB (캐시 누적)
배치 100: 피크 ~90MB, LiteDB 캐시 ~500MB (캐시 누적)
...
배치 500: 피크 ~90MB, LiteDB 캐시 ~?? GB (이론적 무한 증가)
그러나 실제로는 MemoryCache가 오래된 페이지를 _free 큐로 이동시키고, 새 페이지 할당 시 재사용한다. 문제는 _free 큐 자체에 상한이 없다는 점이다.
💡 참고: LiteDB MemoryCache 메모리 누수 #1756, PageBuffer 무한 증가 #2020
🔹 4.3 메모리 예측 요약
| 시점 | 사용자 데이터 | LiteDB 캐시 | Unity 힙 | 총 메모리 |
|---|---|---|---|---|
| 배치 1 | ~55 MB | ~27 MB | ~128 MB | ~210 MB |
| 배치 50 | ~55 MB | ~100~300 MB | ~512 MB | ~670~870 MB |
| 배치 100 | ~55 MB | ~200~500 MB | ~1 GB | ~1.2~1.5 GB |
| 배치 500 (최종) | ~55 MB | ~500 MB~2 GB | ~2 GB | ~2.5~4 GB |
⚠️ 경고: Unity Boehm GC는 힙을 확장만 하고 축소하지 않는다. 500 배치 순회 후 Unity 관리 힙은 최대 확장 크기를 유지한다.
💡 참고: Unity GC 문서
🔹 4.4 메모리 완화 전략
| 전략 | 효과 | 구현 |
|---|---|---|
| DB 인스턴스 재생성 (100배치마다) | 캐시 완전 해제 | db.Dispose() → new LiteDatabase() |
| GC.Collect() (배치 간) | 사용자 객체 회수 (힙 크기 유지) | 프레임 스파이크 유발 주의 |
| 커뮤니티 패치 적용 | free 큐 200개 상한 (1.6MB) | LiteDb5_Memory |
| 배치 간 yield | Unity 프레임 유지 | yield return null |
📌 5. 최적 구현: Keyset Pagination + 메모리 관리
public IEnumerator LoadAllMeshesCoroutine(
ILiteCollection<MyMesh> col,
System.Action<MyMesh> onMeshLoaded)
{
long lastId = 0;
int batchCount = 0;
int totalLoaded = 0;
while (true)
{
// Keyset Pagination: Id 인덱스로 정확한 위치 점프
var batch = col.Find(x => x.Id > lastId, limit: 1000).ToList();
if (batch.Count == 0) break;
// 데이터 처리
foreach (var mesh in batch)
{
onMeshLoaded(mesh);
}
lastId = batch[^1].Id;
totalLoaded += batch.Count;
batchCount++;
// 명시적 참조 해제
batch.Clear();
batch = null;
// Unity 프레임 양보 (매 배치마다)
yield return null;
// 100배치(10만건)마다 GC 강제 수집 (선택)
if (batchCount % 100 == 0)
{
System.GC.Collect();
yield return null;
}
}
Debug.Log($"전체 로딩 완료: {totalLoaded}건, {batchCount}배치");
}
🧪 5.1 예상 성능 (Keyset + byte[] + 위 코드)
| 항목 | 예상치 |
|---|---|
| 총 소요시간 | ~1~2분 (500배치 × ~60ms + yield 오버헤드) |
| 배치당 시간 | ~60~120ms (일정) |
| 배치당 피크 메모리 | ~87~92 MB |
| 누적 LiteDB 캐시 | ~500MB~2GB (재생성 없이) |
| 실용적 총 메모리 | ~500MB~1.5GB (GC.Collect 포함) |
| Unity 프레임 영향 | 매 배치 1프레임 소비 (16ms 초과 시 프레임 드롭) |
🗄️ 5.2 DB 재생성 포함 버전 (메모리 안전)
public IEnumerator LoadAllMeshesWithDbRestart(
string dbPath, System.Action<MyMesh> onMeshLoaded)
{
long lastId = 0;
int totalLoaded = 0;
while (true)
{
// DB 인스턴스 생성 (캐시 초기화)
using (var db = new LiteDatabase($"Filename={dbPath};Connection=direct"))
{
var col = db.GetCollection<MyMesh>("meshes");
int batchInSession = 0;
while (batchInSession < 100) // 100배치(10만건)마다 DB 재생성
{
var batch = col.Find(x => x.Id > lastId, limit: 1000).ToList();
if (batch.Count == 0) yield break;
foreach (var mesh in batch)
onMeshLoaded(mesh);
lastId = batch[^1].Id;
totalLoaded += batch.Count;
batchInSession++;
batch.Clear();
batch = null;
yield return null;
}
} // db.Dispose() → MemoryCache 완전 해제
System.GC.Collect();
yield return null;
}
}
| 항목 | DB 재생성 없음 | DB 100배치마다 재생성 |
|---|---|---|
| 총 소요시간 | ~1~2분 | ~2~3분 (재생성 오버헤드) |
| 피크 메모리 | ~500MB~2GB | ~300~500MB |
| 메모리 안정성 | 계속 증가 | 안정적 |
⚖️ 6. 종합 비교표
🗂️ 50만건 MyMesh (byte[] 저장, 28KB/건, 정점 1,000개)
| 방식 | 총 시간 | 피크 메모리 | 안정성 |
|---|---|---|---|
| FindAll().ToList() | OOM 불가 | OOM (13GB+) | 불가 |
| FindAll() 스트리밍 | ~1~2분 | ~100MB~2GB (캐시 누적) | 위험 |
| Skip/Offset 1000건 | ~20~40분 | ~90MB~2GB (캐시 누적) | 느림 |
| Keyset 1000건 | ~1~2분 | ~90MB~2GB (캐시 누적) | 양호 |
| Keyset + DB 재생성 | ~2~3분 | ~300~500MB | 안전 |
⚖️ 배치 시간 추이 비교
Skip/Offset: Keyset:
배치 1: ▏ 60ms 배치 1: ▏ 60ms
배치 50: ████ 3초 배치 50: ▏ 60ms
배치 100: ████████ 6초 배치 100: ▏ 60ms
배치 250: ████████████████ 15초 배치 250: ▏ 60ms
배치 500: ██████████████████████████████ 30초
배치 500: ▏ 60ms
📌 7. 최종 권장사항
| 우선순위 | 권장사항 |
|---|---|
| 1 (필수) | float[] → byte[] 변환 저장 (BsonBinary) |
| 2 (필수) | Keyset Pagination 사용 (Id > lastId), Skip/Offset 절대 금지 |
| 3 (강력 권장) | 100배치(10만건)마다 DB 인스턴스 Dispose() + 재생성 |
| 4 (권장) | Unity 코루틴에서 매 배치마다 yield return null |
| 5 (선택) | 10만건 처리 후 GC.Collect() (프레임 스파이크 허용 시점) |
이 조합으로 50만건 × 28KB 메시 데이터를 ~2~3분, ~300~500MB 메모리 내에서 안전하게 순회 로딩 가능하다.
🔗 참고 자료
- LiteDB 인덱스 문서
- LiteDB 데이터 구조
- LiteDB vs MongoDB 벤치마크 — 124,537 rec/sec
- SoloDB vs LiteDB 성능 비교
- LiteDB 100만건 10분+ 이슈 #1822
- LiteDB v4 vs v5 쿼리 성능 #2023
- LiteDB v5.0.18 성능 퇴행 #2451
- LiteDB Skip/Limit 이슈 #718
- LiteDB MemoryCache 메모리 누수 #1756
- LiteDB PageBuffer 무한 증가 #2020
- LiteDB 메모리 관리 장기 운용 #2311
- LiteDB 캐시 크기 설정 요청 #1896
- LiteDB 메모리 누수 #1688
- LiteDb5_Memory 커뮤니티 패치
- Unity GC 개요
- Unity Incremental GC
- Unity 관리 힙 동작
- .NET LOH 공식 문서
- DeepWiki — LiteDB 페이지 관리