NSFetchedResultsController and sort performance

I really liked the NSFetchedResultsController when it was introduced in iPhone OS 3.0. It has made it much easier to implement a core data backed table view and removes the need to write a lot of code. It also seems to do a pretty good job of keeping the memory footprint to a minimum.

However, one thing I have been struggling with is with performance when using a grouped table. To be more precise the issue is not really with the fetched results controller but with the sort descriptor when using a case insensitive search. The basic setup is as follows:

// Create a fetch request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Record"
                     inManagedObjectContext:moc];
[fetchRequest setEntity:entity];

// Create a sort descriptor for the request
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
                  initWithKey:@"value"
                    ascending:YES
                     selector:@selector(localizedCaseInsensitiveCompare:)];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];

// Now create the fetched results controller
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc]
                            initWithFetchRequest:fetchRequest
                            managedObjectContext:sharedMoc
                              sectionNameKeyPath:@"valueForSectionTitle"
                                       cacheName:@"cache"];

[fetchRequest release];
[sortDescriptor release];

The model named Record has a string attribute named value that is used to order the contents of the table. A method defined for the Record model valueForSectionTitle returns the value used for generating the section title for each record. It looks something like this (assuming UTF8 strings):

- (NSString *)valueForSectionTitle {
  // Return the first character of the value
  // converted to uppercase
  return [[self.value substringToIndex:1] uppercaseString];
}

This approach works but even with small datasets of around 1,000 records there is a noticeable lag in the user interface when the table view loads. Turning on core data SQL debugging shows what is happening:

2010-03-15 14:12:28.633 CorePerf[2367:207] CoreData: sql: SELECT 0, t0.Z_PK,
 t0.Z_OPT, t0.ZVALUE FROM ZRECORD t0 ORDER BY t0.ZVALUE COLLATE 
 NSCollateLocaleSensitiveNoCase
2010-03-15 14:12:29.654 CorePerf[2367:207] CoreData: annotation: sql
 connection fetch time: 0.8731s
2010-03-15 14:12:29.662 CorePerf[2367:207] CoreData: annotation: total fetch
 execution time: 1.0311s for 1500 rows.

So for 1500 records it takes over a second for the fetch request to execute. To avoid this runtime delay it make sense to compute the section index title (in this the uppercase initial letter of the string) ahead of time and avoid the case insensitive search. Adding a new attribute to the core data model to hold this initial letter simplifies the sort descriptor removing the need for a sort comparison selector.

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
                  initWithKey:@"indexValue"
                    ascending:YES];

The impact on the fetch request is dramatic:

2010-03-15 14:21:25.580 CorePerf[2397:207] CoreData: sql: SELECT 0, t0.Z_PK,
 t0.Z_OPT, t0.ZVALUE, t0.ZINDEXVALUE FROM ZRECORD t0 ORDER BY t0.ZINDEXVALUE
2010-03-15 14:21:25.925 CorePerf[2397:207] CoreData: annotation: sql
 connection fetch time: 0.2101s
2010-03-15 14:21:25.934 CorePerf[2397:207] CoreData: annotation: total
 fetch execution time: 0.3537s for 1327 rows.

The fetch time drops from 0.8731s to 0.2102s which is a 75% improvement. When I get some more time I will experiment with some different data set sizes but for now this seems to suggest a definite conclusion:

Arrange your data so that you can use the default sort comparison selector when creating core data fetch requests.