如何在保持高效的同时在 NSPredicates 中使用 AND 和 ANY

How to use AND with ANY in NSPredicates while staying efficient

假设我有一本 collection 核心数据书籍。每本书可以有多个作者。这是想象中的 collection 本书的样子(它只有一本书,只是为了简化事情):

[
    Book(authors: [
        Author(first: "John", last: "Monarch"),
        Author(first: "Sarah", last: "Monarch")
    ])
]

我想过滤我的 collection 书籍,只筛选出作者姓名为“Sarah Monarch”的书籍。

据我目前所见,如果我想写一个 NSPredicate 来过滤我的 collection 和 return 一个只包含这个的过滤 collection本书,我可以使用 SUBQUERY:

NSPredicate(format: "SUBQUERY(authors, $author, $author.#first == 'Sarah' && $author.#last == 'Monarch').@count > 0")

我的理解是这个操作本质上是一样的:

books.filter {
    book in
    
    let matchingAuthors = book.authors.filter {
        author in
        
        author.first == "John" && author.last == "Monarch"
    }
    
    return matchingAuthors.count > 0
}

我的问题是这里似乎有些低效 — SUBQUERY(以及上面的示例代码)将查看 所有 作者,当我们找到后可以停止只有一个匹配。我的直觉会引导我尝试像这样的谓词:

ANY (authors.#first == "Sarah" && authors.#last == "Monarch")

作为代码,可以是:

books.filter {
    book in
    
    return book.authors.contains {
        author in
        
        author.first == "John" && author.last == "Monarch"
    }
}

但是这个谓词的语法无效。

如果我是对的,基于 SUBQUERY 的方法效率较低(因为它查看 collection 中的所有元素,而不是仅仅在第一个匹配项处停止)是吗更正确、更有效的方法?

以下是另一种方法,它不使用 SUBQUERY,但最终应该有相同的结果。我不知道这在实践中是否会比使用 SUBQUERY 更有效或更不有效。

您特别担心的是计算所有匹配作者的效率低下,而不是仅在找到第一个匹配作者时停止。请记住,每当处理提取请求时,幕后都会发生很多事情。首先,CoreData 必须解析您的谓词并将其转换为等效的 SQLite 查询,它看起来像这样:

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, ... FROM ZBOOK t0 WHERE (SELECT COUNT(t1.Z_PK) FROM ZAUTHOR t1 WHERE (t0.Z_PK = t1.ZBOOK AND ( t1.ZFIRST == 'Sarah' AND  t1.ZLAST == 'Monarch')) ) > 0

(具体查询要看是否定义了逆关系,定义了是一对多还是多对多;以上是基于一对一逆关系,book).

当 SQLite 被交给要执行的查询时,它将检查它有哪些索引可用,然后调用查询规划器来确定如何最好地处理它。感兴趣的是 subselect 来获得计数:

SELECT COUNT(t1.Z_PK) FROM ZAUTHOR t1 WHERE (t0.Z_PK = t1.ZBOOK AND ( t1.ZFIRST == 'Sarah' AND  t1.ZLAST == 'Monarch'))

这是与您的第一个代码片段对应的查询部分。请注意,在 SQLite 术语中,它是一个相关的子查询:它包括一个来自外部 SELECT 语句的参数(t0.Z_PK - 这本质上是相关的书)。 SQLite 将搜索整个 table 作者,首先查看他们是否与该书相关,然后查看作者的名字和姓氏是否匹配。你的提议是这是低效的;一旦找到任何匹配的作者,嵌套的 select 就会停止。在 SQLite 术语中,这将对应于这样的查询:

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, ... FROM ZBOOK t0 WHERE EXISTS(SELECT 1 FROM ZAUTHOR t1 WHERE (t0.Z_PK = t1.ZBOOK AND ( t1.ZFIRST == 'Sarah' AND  t1.ZLAST == 'Monarch')) )

(从SQLite docs whether the EXISTS operator actually shortcuts the underlying subselect, but this answer elsewhere on SO suggests it does. If not it might be necessary to add "LIMIT 1" to the subselect to get it to stop after one row is returned). The problem is, I don't know of any way to craft a CoreData predicate which would be converted into a SQLite query using the EXISTS operator. Certainly it's not listed as a function in the NSExpression documentation, nor is it mentioned (or listed as a reserved word) in the predicate format documentation. Likewise, I don't know of any way to add "LIMIT 1" to the subquery (though it's relatively straightforward to add to the main fetch request using fetchLimit看不清楚。

因此,解决您确定的问题的余地不大。但是,可能存在其他低效率。依次扫描每本书的作者 table(相关子查询)可能是一个。扫描作者 table 一次,确定符合相关标准(first = “Sarah” 和 last = “Monarch”),然后使用它(大概更短)列表来搜索书籍?正如我在开始时所说,这是一个悬而未决的问题:我不知道它是否更有效率。

要将一个获取请求的结果传递给另一个,请使用 NSFetchRequestExpression。这有点神秘,但希望以下代码足够清晰:

    let authorFetch = Author.fetchRequest()
    authorFetch.predicate = NSPredicate(format: "#first == 'Sarah' AND #last == 'Monarch'")
    authorFetch.resultType = .managedObjectIDResultType
    let contextExp = NSExpression(forConstantValue: self.managedObjectContext)
    let fetchExp = NSExpression(forConstantValue: authorFetch)
    let fre = NSFetchRequestExpression.expression(forFetch: fetchExp, context: contextExp, countOnly: false)
    let bookFetch = Book.fetchRequest()
    bookFetch.predicate = NSPredicate(format: "ANY authors IN %@", fre)
    let results = try! self.managedObjectContext!.fetch(bookFetch)

使用该提取的结果是这样的 SQL 查询:

SELECT DISTINCT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, ... FROM ZBOOK t0 JOIN ZAUTHOR t1 ON t0.Z_PK = t1.ZBOOK WHERE  t1.Z_PK IN (SELECT n1_t0.Z_PK FROM ZAUHTOR n1_t0 WHERE ( n1_t0.ZFIST == 'Sarah' AND  n1_t0.ZLAST == 'Monarch')

这有其自身的复杂性(DISTINCT、JOIN 和 subselect)但重要的是 subselect 不再相关:它独立于外部 SELECT 所以可以计算一次而不是为外部的每一行重新计算 SELECT.