본문 바로가기

Computer

[RealmSwift] Index 사용에 대한 성능 테스트 기록

RealmSwift 10.47.0 기준으로 작성된 글입니다.

 

[READ]

No indexing

                                       총 Item 갯수 
                                 \ 
Query 실행 횟수
50,000 100,000 200,000
5,000 2.377 (초) 4.388 8.310
10,000 4.689 8.703 16.429
20,000 9.376 16.963 32.514

 

Yes Indexing

                                       총 Item 갯수 
                                 \ 
Query 실행 횟수
50,000 100,000 200,000
5,000 0.436 0.440 0.435
10,000 0.874 0.873 0.906
20,000 1.749 1.755 1.773

 

[Write]

No Indexing

Query 실행 횟수 소요 시간
1,000 2.673
5,000 16.570
10,000 31.701
20,000 70.368

 

Yes Indexing

Query 실행 횟수 소요 시간
1,000 2.534
5,000 18.546
10,000 32.907
20,000 70.370

 

 

 

일단 쓰기 작업은 거의 차이가 없었다. 아마도 텍스트 인덱스 단 하나를 추가했기때문에 인덱싱을 위한 추가 작업 시간이 무시해도 될 정도 였던걸까?(잘 모르겠다.)

하지만 읽기 작업에서는 큰 차이를 확인할 수 있었다. 인덱스를 사용하지 않았을 때는 Item 갯수가 적음에도 갯수가 N배 증가함에따라 소요시간도 N배 증가하는 추세를 보였지만, 인덱스를 사용했을 때는 200,000개 이하 Item 갯수에서 갯수 변화에 따른 소요시간에 큰 변화도 없었고 전체적인 소요시간이 굉장히 많이 줄어들었다.

 

 

import Foundation
import Realm
import RealmSwift
import XCTest

/// Read 테스트를 위해 준비할 아이템 갯수
fileprivate let PREPARED_ITEM_COUNT = 100_000 // 변경 가능

/// 테스트 중 쿼리가 실행되는 횟수
fileprivate let QUERY_EXECUTION_COUNT = 5_000 // 변경 가능

/// 실제 테스트가 실행되는 범위. 1에서 `QUERY_EXECUTION_COUNT`까지 무작위로 섞는다.
fileprivate let TEST_SCOPE = (1...QUERY_EXECUTION_COUNT).shuffled()

final class RealmPerformanceTests: XCTestCase {
    
    var sut: Realm!
    var config: Realm.Configuration!
    
    override func setUpWithError() throws {
        try super.setUpWithError()
        
        self.continueAfterFailure = false
    }
    
    override func tearDownWithError() throws {
        try super.tearDownWithError()
        
        sut = nil
        config = nil
    }
    
    func test_performanceWithoutIndex_whenWrite() throws {
        // Arrange
        config = .init()
        config.fileURL?.deleteLastPathComponent()
        config.fileURL?.append(path: "RealmWriteTestsWithoutIndex")
        config.fileURL?.appendPathExtension("realm")
        
        sut = try! .init(configuration: config)
//        print(config.fileURL!)
        
        defer { // 측정 대상 보존 상태 검증
            let objects = sut.objects(Item.self)
            XCTAssertTrue(objects.isEmpty)
        }
        
        // Act
        self.measure {
            do { // 측정 대상 준비 상태 검증
                let objects = sut.objects(Item.self)
                XCTAssertTrue(objects.isEmpty)
            }
            
            TEST_SCOPE.forEach { number in
                let newItem: Item = .init(number: "\(number)")
                
                do {
                    try sut.write {
                        sut.add(newItem)
                    }
                } catch {
                    XCTFail("저장 실패.")
                }
            }
            
            do {
                try sut.write {
                    sut.deleteAll()
                }
            } catch {
                XCTFail("삭제 실패.")
            }
        }
    }
    
    func test_performanceWithIndex_whenWrite() throws {
        // Arrange
        config = .init()
        config.fileURL?.deleteLastPathComponent()
        config.fileURL?.append(path: "RealmWriteTestsWithIndex")
        config.fileURL?.appendPathExtension("realm")
        
        sut = try! .init(configuration: config)
//        print(config.fileURL!)
        
        defer { // 측정 대상 보존 상태 검증
            let objects = sut.objects(IndexingItem.self)
            XCTAssertTrue(objects.isEmpty)
        }
        
        // Act
        self.measure {
            do { // 측정 대상 준비 상태 검증
                let objects = sut.objects(IndexingItem.self)
                XCTAssertTrue(objects.isEmpty)
            }
            
            TEST_SCOPE.forEach { number in
                let newItem: IndexingItem = .init(number: "\(number)")
                
                do {
                    try sut.write {
                        sut.add(newItem)
                    }
                } catch {
                    XCTFail("저장 실패.")
                }
            }
            
            do {
                try sut.write {
                    sut.deleteAll()
                }
            } catch {
                XCTFail("삭제 실패.")
            }
        }
    }
    
    func test_performanceWithoutIndex_whenRead() throws {
        // Arrange
        config = .init()
        config.fileURL?.deleteLastPathComponent()
        config.fileURL?.append(path: "RealmReadTestsWithoutIndex")
        config.fileURL?.appendPathExtension("realm")
        
        sut = try! .init(configuration: config)
//        print(config.fileURL!)
        
        XCTAssertGreaterThanOrEqual(PREPARED_ITEM_COUNT, QUERY_EXECUTION_COUNT, "준비된 아이템 갯수가 Query 실행 횟수보다 많아야합니다.")
        
        if sut.objects(Item.self).count != PREPARED_ITEM_COUNT {
            try sut.write {
                sut.deleteAll()
                (1...PREPARED_ITEM_COUNT).forEach { number in
                    let item: Item = .init(number: "\(number)")
                    sut.add(item)
                }
            }
        }
        
        // Act
        self.measure {
            TEST_SCOPE.forEach { number in
                let objects = sut.objects(Item.self)
                let object = objects.where { $0.number.equals("\(number)") }
                XCTAssertNotNil(object)
            }
        }
    }
    
    func test_performanceWithIndex_whenRead() throws {
        // Arrange
        config = .init()
        config.fileURL?.deleteLastPathComponent()
        config.fileURL?.append(path: "RealmReadTestsWithIndex")
        config.fileURL?.appendPathExtension("realm")
        
        sut = try! .init(configuration: config)
//        print(config.fileURL!)
        
        XCTAssertGreaterThanOrEqual(PREPARED_ITEM_COUNT, QUERY_EXECUTION_COUNT, "준비된 아이템 갯수가 Query 실행 횟수보다 많아야합니다.")
        
        if sut.objects(Item.self).count != PREPARED_ITEM_COUNT {
            try sut.write {
                sut.deleteAll()
                (1...PREPARED_ITEM_COUNT).forEach { number in
                    let item: IndexingItem = .init(number: "\(number)")
                    sut.add(item)
                }
            }
        }
        
        // Act
        self.measure {
            TEST_SCOPE.forEach { number in
                let objects = sut.objects(IndexingItem.self)
                let object = objects.where { $0.number.equals("\(number)") }
                XCTAssertNotNil(object)
            }
        }
    }
}

final class Item: Object {
    
    @Persisted(primaryKey: true) var id: ObjectId = .generate()
    
    @Persisted var number: String
    
    convenience init(number: String) {
        self.init()
        self.number = number
    }
}

final class IndexingItem: Object {
    
    @Persisted(primaryKey: true) var id: ObjectId = .generate()
    
    @Persisted(indexed: true) var number: String
    
    convenience init(number: String) {
        self.init()
        self.number = number
    }
}

사용된 테스트 코드