2015-11-02 115 views
0

我從Core Data獲取幾千個對象,而我只想返回那些至少有1個與它相關的對象。核心數據:使用關係計數謂詞獲取性能較差

當我使用類似於以下的謂詞時,獲取對象需要很長時間。周圍5-8秒:

NSPredicate(format: "[email protected] > 0") 

是否有執行該擷取的更有效的方式,還是應該在快速查找(即hasRelatedObjects屬性)對象緩存的值。

如果緩存是最好的路線,我不相信它是微不足道的。如果我修改我的Tag對象,例如在willSave中,我可以獲取關係數並將其存儲在我的新屬性中。但是,如果相關對象在關係的一側將標記添加到自身,則Tag對象不會更改,因此willSave將不會被調用。

如何確保您是否撥打myTag.addRelatedObject(obj)myTag對象已更新)或myObj.addRelatedTag(myTag)myObj已更新),該值是否已緩存?

回答

2

首先,我們來做一下原型設計,看看這個提取是在做什麼。我假設你正在使用SQLite存儲。

我砍了一個快速模型,類似於你所描述的。

我定義了一個與Subentity具有多對多關係的實體,其中該Subentity具有一對一的關係。

現在,我正在模擬器中進行測試,所以我創建了一個包含10mm實體的數據庫。每創建一個新實體,它至少有2%的機會爲其創建至少一個子實體。如此選擇的每個實體隨機獲得1到10個子實體。

因此,我結束了一個擁有10,000,000個Entity對象的數據庫和1,101,223個Subentity對象,其中199,788個Entity對象在其關係中至少有一個Subentity。


對於最簡單讀取請求時(同爲一個在你的例子),我們得到這個代碼...

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entity"]; 
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"[email protected] != 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. 

如果您對SQL有所瞭解,則可以看到查詢不是最優的。兩張桌子底下都有太多的事情要做。

如果我們簡單地爲關係數量添加一個緩存,我們會得到這個結果(注意表格沒有在count上編制索引)。

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entity"]; 
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount != 0"]; 
NSArray *results = [moc executeFetchRequest:fetchRequest error:NULL]; 

那麼我們得到這些結果...

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. 

現在,讓我們看看會發生什麼,如果我們索引subcount領域。

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.5749s 
CoreData: annotation: total fetch execution time: 1.5788s 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. 

改變謂詞回到這個

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: 1.5619s 
CoreData: annotation: total fetch execution time: 1.5657s for 199788 rows. 

而這一次

fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount == 1"]; 

產量

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之外操作對象。

那麼,注意到計算值需要更新的一種方法是當對象保存時。您可以覆蓋willSave並在那裏進行必要的更改。您還可以觀察上下文將保存通知並在那裏進行工作。

對我來說,這種方法的主要問題是,「將保存」通知發生在驗證和合並持久性存儲之前。這些進程中的任何一個都可能改變數據,並且有一些棘手的合併問題可能會導致問題。

真正確保驗證和合並已成爲核心的唯一方法是掛鉤驗證階段。

不幸的是,Apple文檔強烈建議不要這種方法。儘管如此,我已經取得了很好的成功。如果您想了解更多「頻繁的錯誤:)從對象,做了變更通知(或其他任何地方爲此事):

- (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 「不僅在保存時保持一致。


對於你關於改變一對一關係的問題,核心數據將正確處理反向關係。而且,所涉及的所有對象都會反映出適當的變化。

具體來說,如果你改變一個子實體的一對一關係。現在您將擁有三個更新的對象:子實體本身,曾經位於關係另一端的實體以及現在位於關係另一端的實體。

+0

太棒了,非常感謝您提供深入的信息!真的很有用。努力弄清楚骨頭上剩下的肉是哪裏,但哈哈:-) – Sencha

+0

TL; DR--你能在頂部添加一個摘要嗎? – Mundi

1

你當然有定義反向關係,對吧?所以應該調用你的關係的didSet處理程序,即使它從另一端改變。

的確,我認爲willSave也應該被調用。你確認它不是?