Core data 核心数据:使用关系计数谓词获取性能差
我正在从核心数据中提取几千个对象,我只想返回那些至少有一个对象与其相关的对象 当我使用类似于下面的谓词时,获取对象需要很长时间。大约5-8秒:Core data 核心数据:使用关系计数谓词获取性能差,core-data,Core Data,我正在从核心数据中提取几千个对象,我只想返回那些至少有一个对象与其相关的对象 当我使用类似于下面的谓词时,获取对象需要很长时间。大约5-8秒: NSPredicate(format: "relationName.@count > 0") 是否有更有效的方法执行此提取,或者是否应将值缓存在对象中以进行快速查找(即hasRelatedObjects属性) 如果缓存是最好的途径,我不认为它微不足道。例如,如果我修改我的标记对象,在willSave中,我可以获取关系计数并将其存储在我的新属性中。
NSPredicate(format: "relationName.@count > 0")
是否有更有效的方法执行此提取,或者是否应将值缓存在对象中以进行快速查找(即hasRelatedObjects
属性)
如果缓存是最好的途径,我不认为它微不足道。例如,如果我修改我的标记
对象,在willSave
中,我可以获取关系计数并将其存储在我的新属性中。但是,如果相关对象将标记添加到其自身的关系一侧,则标记
对象永远不会更改,因此将保存
我如何确保无论您调用
myTag.addRelatedObject(obj)
(myTag
对象已更新)还是myObj.addRelatedTag(myTag)
(myObj
已更新),都会缓存该值?您肯定定义了反向关系,对吗?因此,即使从另一端更改了关系上的didSet
处理程序,也应该调用它
事实上,我认为还应该调用
willSave
。你确认了吗?你肯定已经定义了反向关系,对吗?因此,即使从另一端更改了关系上的didSet
处理程序,也应该调用它
事实上,我认为还应该调用
willSave
。您验证了它不是吗?首先,让我们先做一点原型设计,看看这个fetch在做什么。我假设您使用的是SQLite存储
我入侵了一个快速模型,与你描述的相似
我定义了一个实体,它与子实体之间有一对多的关系,而子实体与子实体之间有一对一的反向关系
现在,我在模拟器中进行测试,所以我创建了一个包含10mm实体的数据库。每次创建一个新实体时,至少有2%的几率为其创建一个子实体。这样选择的每个实体随机得到1到10个子实体
因此,我得到了一个包含10000000个实体对象和1101223个子实体对象的数据库,其中199788个实体对象的关系中至少有一个子实体
对于最简单的获取请求(与您的示例中的请求相同),我们得到以下代码
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entity"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subentities.@count != 0"];
NSArray *results = [moc executeFetchRequest:fetchRequest error:NULL];
以及生成的SQL,以及执行获取所需的时间
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE (SELECT COUNT(t1.Z_PK) FROM ZSUBENTITY t1
WHERE (t0.Z_PK = t1.ZENTITY) ) <> ?
CoreData: annotation: sql connection fetch time: 17.9598s
CoreData: annotation: total fetch execution time: 17.9657s for 199788 rows.
然后我们得到这些结果
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT <> ?
CoreData: annotation: sql connection fetch time: 1.5795s
CoreData: annotation: total fetch execution time: 1.5838s for 199788 rows.
嗯,没有好多少。如果我们稍微改变一下谓词
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT > ?
CoreData: annotation: sql connection fetch time: 0.7805s
CoreData: annotation: total fetch execution time: 0.7843s for 199788 rows.
现在,这花了一半的时间。我不太清楚为什么,因为即使较慢的路径进行了两次二进制搜索,也没有值小于0的记录
而且,我希望有更好的改进,基于这样一个事实:使用排序索引,它应该能够进行二进制搜索,这应该比完全线性扫描速度的一半要好得多
不管怎么说,这确实表明,它可以更快
看看我们的下限是多少,我们可以这样做
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Test"];
fetchRequest.fetchLimit = 199788;
NSArray *results = [moc executeFetchRequest:fetchRequest error:NULL];
这就给出了这些结果,这是我们所能获得的最好的结果,因为它基本上不进行搜索
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 LIMIT 199788
CoreData: annotation: sql connection fetch time: 0.1284s
CoreData: annotation: total fetch execution time: 0.1364s for 199788 rows.
现在,如果我们只关心它们是否为空,而不关心实际计数,我们可以将缓存计数改为布尔值,它总是0或1
通过采用这种方法,我们可以使用谓词获取
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount > 0"];
屈服
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT > ?
CoreData: annotation: sql connection fetch time: 0.5312s
CoreData: annotation: total fetch execution time: 0.5351s for 199788 rows.
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT <> ?
CoreData: annotation: sql connection fetch time: 1.5619s
CoreData: annotation: total fetch execution time: 1.5657s for 199788 rows.
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT = ?
CoreData: annotation: sql connection fetch time: 0.5332s
CoreData: annotation: total fetch execution time: 0.5366s for 199788 rows.
将谓词更改回此
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount != 0"];
屈服
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT > ?
CoreData: annotation: sql connection fetch time: 0.5312s
CoreData: annotation: total fetch execution time: 0.5351s for 199788 rows.
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT <> ?
CoreData: annotation: sql connection fetch time: 1.5619s
CoreData: annotation: total fetch execution time: 1.5657s for 199788 rows.
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT = ?
CoreData: annotation: sql connection fetch time: 0.5332s
CoreData: annotation: total fetch execution time: 0.5366s for 199788 rows.
屈服
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT > ?
CoreData: annotation: sql connection fetch time: 0.5312s
CoreData: annotation: total fetch execution time: 0.5351s for 199788 rows.
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT <> ?
CoreData: annotation: sql connection fetch time: 1.5619s
CoreData: annotation: total fetch execution time: 1.5657s for 199788 rows.
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT = ?
CoreData: annotation: sql connection fetch time: 0.5332s
CoreData: annotation: total fetch execution time: 0.5366s for 199788 rows.
所以,骨头上还有一些肉,但我会给你一些乐趣
好的,既然我们想要缓存这些更改,那么我们如何才能做到这一点呢 嗯,最简单的方法就是提供一个自定义方法,每次关系发生变化时都会使用该方法。然而,它随后要求所有的更改都经过这个过程,并且总是有可能某些代码在特殊API之外操纵对象 注意到计算值需要更新的一种方法是当对象保存时。您可以覆盖
将保存,并在那里进行任何必要的更改。您还可以观察上下文将保存通知并在其中执行工作
对我来说,这种方法的主要问题是“将保存”通知发生在验证和与持久存储合并之前。这两个过程中的任何一个都可能更改数据,并且存在一些可能导致问题的棘手合并问题
真正确保验证和合并成为核心的唯一方法是连接到验证阶段
不幸的是,苹果的文档强烈反对这种方法。不过,我在这种模式下取得了很好的成功
- (BOOL)validateSubcount:(id*)ioValue error:(NSError**)outError
{
NSUInteger computedValue = [*ioValue unsignedIntegerValue];
NSUInteger actualValue = computedValue;
NSString *key = @"subentities";
if ([self hasFaultForRelationshipNamed:key]) {
if (self.changedValues[@"subcount"]) {
if (has_objectIDsForRelationshipNamed) {
actualValue = [[self objectIDsForRelationshipNamed:key] count];
} else {
actualValue = [[self valueForKey:key] count];
}
}
} else {
actualValue = [[self valueForKey:key] count];
}
if (computedValue != actualValue) {
*ioValue = @(actualValue);
}
return YES;
}
保存时会自动调用该函数,如果您希望更频繁地保持一致性,而不仅仅是在保存时,则可以从“对象已更改”通知(或其他任何地方)手动调用该函数(通过validateValue:forKey:error:)
关于您关于将关系更改为一种关系的问题;核心数据将正确处理反向关系。此外,所有涉及的对象都将反映出适当的更改
特别是,如果将子实体的关系更改为一个关系。现在,您将有三个更新的对象:子实体本身、以前位于关系另一端的实体以及现在位于关系另一端的实体。首先,让我们做一点原型设计,看看提取正在做什么。我假设您使用的是SQLite存储
我入侵了一个快速模型,与你描述的相似
我定义了一个与子实体有对多关系的实体,其中子实体有对一关系