You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何为Realm Object添加索引无法加速计数查询?

Realm索引性能未达预期的问题分析

问题背景

我通过代码测试Realm索引效果:创建两个Realm对象类,一个Int类型字段val带索引,另一个不带索引。分别向两个类插入100万条值均匀分布的数据后,测试两种查询方式(filter字符串、where闭包)的计数耗时。

测试结果显示,带索引与不带索引的查询耗时基本一致(差异在5%以内),远未达到预期的O(logN)级速度提升。后续调整测试后,索引版本仅获得约33%的速度提升,想了解问题出在哪里。

测试代码

import RealmSwift
class TimeIt {
  let val = DispatchTime.now()
  func elapsed() -> DispatchTimeInterval {
    return val.distance(to: DispatchTime.now())
  }

  static func time(_ desc: String, aclosure: () -> Void) {
    let t = TimeIt()
    aclosure()
      print("\(desc) took: \(Double(t.elapsed().nanoseconds) / 1000000.0) ms")
  }
}

class RealmIndexed: Object {
  @Persisted var id = UUID().uuidString
  @Persisted(indexed: true) var val: Int = 0
}

class RealmUnindexed: Object {
  @Persisted var id = UUID().uuidString
  @Persisted var val: Int = 0
}

func generateObjs(_ hundredsOfThousands: Int = 1) {
  let realm = try! Realm()
  var objs: [RealmIndexed] = []
  for _ in 0..<hundredsOfThousands {
    for i in 0..<100_000 {
      let obj = RealmIndexed()
      obj.val = i
      objs.append(obj)
    }
  }
  try! realm.write {
    realm.add(objs)
  }
  var objs2: [RealmUnindexed] = []
  for _ in 0..<hundredsOfThousands {
    for i in 0..<100_000 {
      let obj = RealmUnindexed()
      obj.val = i
      objs2.append(obj)
    }
  }
  try! realm.write {
    realm.add(objs2)
  }
}

func baseCountIndexed() -> Int {
  return (try! Realm().objects(RealmIndexed.self)).count
}

func baseCountUnindexed() -> Int {
  return (try! Realm().objects(RealmUnindexed.self)).count
}

func getCountIndexedObjsViaFilterString(_ minVal: Int = 50000) -> Int {
  let count = (try! Realm().objects(RealmIndexed.self).filter("val >= %@", minVal)).count
  return count
}

func getCountUnindexedObjsViaFilterString(_ minVal: Int = 50000) -> Int {
  let count = (try! Realm().objects(RealmUnindexed.self).filter("val >= %@", minVal)).count
  return count
}

func getCountIndexedObjsViaWhereClosure(_ minVal: Int = 50000) -> Int {
  let count = (try! Realm().objects(RealmIndexed.self).where { $0.val >= minVal }).count
  return count
}

func getCountUnindexedObjsViaWhereClosure(_ minVal: Int = 50000) -> Int {
  let count = (try! Realm().objects(RealmUnindexed.self).where { $0.val >= minVal }).count
  return count
}

func testRealmSpeed() {
  TimeIt.time("realm init6") { _ = try! Realm() }
  TimeIt.time("baseCountIndexed") { print(baseCountIndexed()) }
  TimeIt.time("baseCountUnindexed") { print(baseCountUnindexed()) }
  TimeIt.time("getCountIndexedObjsViaFilterString") { print(getCountIndexedObjsViaFilterString()) }
  TimeIt.time("getCountIndexedObjsViaWhereClosure") { print(getCountIndexedObjsViaWhereClosure()) }
  TimeIt.time("getCountUnindexedObjsViaFilterString") { print(getCountUnindexedObjsViaWhereClosure()) }
  TimeIt.time("getCountUnindexedObjsViaWhereClosure") { print(getCountUnindexedObjsViaWhereClosure()) }
}

generateObjs(10)
testRealmSpeed()

问题原因分析

  • Realm延迟加载与计数逻辑:Realm的Results是惰性计算的,调用.count时,即使使用索引,Realm仍需要遍历匹配的对象条目来统计数量,而非直接从索引节点获取聚合值。索引主要优化的是快速定位结果集的起始位置,而非计数操作本身。
  • 查询范围过大:测试中使用的val >= 50000匹配了50%的数据,当结果集占比极高时,遍历大量对象的开销会抵消索引带来的定位优势。如果测试更窄的查询范围(比如val == 99999),索引的性能提升会更显著。
  • 重复创建Realm实例:每个查询函数都重新初始化Realm实例,带来额外的初始化开销,可能掩盖索引的真实性能差异。
  • 缓存与测试顺序影响:首次查询会触发磁盘IO,后续查询受益于缓存。测试先执行索引版本再执行无索引版本,缓存可能让无索引版本的耗时被低估,导致差异不明显。
  • 索引实现特性:Realm的B树索引主要用于快速定位数据,而非存储聚合统计信息,因此计数操作无法直接利用索引的O(logN)特性。

优化建议

  • 复用同一个Realm实例进行所有查询,避免重复初始化的开销。
  • 调整查询条件为更精准的范围(如等值查询、窄范围查询),观察索引的真实性能提升。
  • 多次运行测试并取平均值,排除首次IO和缓存的干扰。
  • 测试实际遍历结果集的耗时,而非仅统计计数,索引对数据访问的提升会更直观。

内容的提问来源于stack exchange,提问作者Kem Mason

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 06:45:33