Core data 核心数据:使用关系计数谓词获取性能差

Core data 核心数据:使用关系计数谓词获取性能差,core-data,Core Data,我正在从核心数据中提取几千个对象,我只想返回那些至少有一个对象与其相关的对象 当我使用类似于下面的谓词时,获取对象需要很长时间。大约5-8秒: NSPredicate(format: "relationName.@count > 0") 是否有更有效的方法执行此提取,或者是否应将值缓存在对象中以进行快速查找(即hasRelatedObjects属性) 如果缓存是最好的途径,我不认为它微不足道。例如,如果我修改我的标记对象,在willSave中,我可以获取关系计数并将其存储在我的新属性中。

我正在从核心数据中提取几千个对象,我只想返回那些至少有一个对象与其相关的对象

当我使用类似于下面的谓词时,获取对象需要很长时间。大约5-8秒:

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存储

我入侵了一个快速模型,与你描述的相似

我定义了一个与子实体有对多关系的实体,其中子实体有对一关系