Real World Peformance of SQL vs Core Data: Need Perspective and Perhaps Specific Fixes - objective-c

I have a word game app that accesses a list of words and clues, which is about 100K entries. The data base is only accessed, never changed. I built the app using SQL methods and it performs pretty well on iOS 6 but the time to get a new clue from the data base is extremely slow on iOS 5:
iOS 5, getting one record from the 100K using SQL, takes about 12
seconds.
iOS 6, getting one record from the 100K using SQL, takes
about 700-1000 milliseconds.
Both of these are on 32 GB iPod Touch.
Given this performance, I made a version using Core Data. My approach gets a random data base entry by first counting the records that fit the query, then chosing one at random. Code follows. Everything I read suggested that Core Data would be faster:
iOS 5, counting records takes around 4 seconds, and retrieving one of
those records takes about 50 - 1500 millisecs. A total time of about 5 seconds.
iOS 6, counting records takes a bit over 2 seconds, and retrieving
one of those records takes about 300-500 millisecs. A total of about 3 seconds.
So Core Data is faster on iOS 5 but slower on iOS 6, compared to SQL. Either way the performance is too slow as far as I am concerned. I know the overhead comes from the methods given below (for the Core Data version). So, two questions:
Any general advice about this issue with an eye to understanding it and improving performance?
Specifically, what about the Core Data code appended below: have I done something foolish that slows it down? Or something else I should add to speed it up? This is my first attempt at Core Data.
Thanks.
- (NSArray *) randomClue {
NSManagedObjectContext* context = [self managedObjectContext];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"A"];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"WL28"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [self createSearchQuery];
[fetchRequest setPredicate:predicate];
NSError *error;
NSDate *date1 = [NSDate date];
NSString *timeString1 = [formatter stringFromDate:date1];
int resCount = [context countForFetchRequest:fetchRequest
error:&error];
NSDate *date2 = [NSDate date];
NSString *timeString2 = [formatter stringFromDate:date2];
int t1 = [timeString1 intValue];
int t2 = [timeString2 intValue];
int d1 = t2-t1;
NSLog(#"randomClue:");
NSLog(#" Time to count array entries: %i", d1);
int ranNum = arc4random_uniform(resCount-1);
int ranNum2 = ranNum + 1;
// Now we fetch just one answer object, not a whole database or even a piece of it!
[fetchRequest setReturnsObjectsAsFaults:YES];
[fetchRequest setPropertiesToFetch:nil];
[fetchRequest setFetchLimit:1];
[fetchRequest setFetchOffset:ranNum2];
NSDate *date3 = [NSDate date];
NSString *timeString3 = [formatter stringFromDate:date3];
self.wl28 = [context executeFetchRequest:fetchRequest error:&error];
NSDate *date4 = [NSDate date];
NSString *timeString4 = [formatter stringFromDate:date4];
int t3 = [timeString3 intValue];
int t4 = [timeString4 intValue];
int d2 = t4-t3;
NSLog(#" Time to retrieve one entry: %i", d2);
return self.wl28;
}
EDIT: createSearchQuery added below
- (NSPredicate *)createSearchQuery {
NSMutableArray *pD = [[GameData gameData].curData valueForKey:#"persData"];
NSNumber *currMin = [pD objectAtIndex:0];
NSNumber *currMax = [pD objectAtIndex:1];
NSNumber *dicNo = [pD objectAtIndex:2];
NSString *dict = nil;
if ([dicNo intValue] == 0) dict = #"TWL";
if ([dicNo intValue] == 1) dict = #"LWL";
NSPredicate *dictPred = [NSPredicate predicateWithFormat:#"dict == %#", dict];
NSPredicate *lowNoCPred = [NSPredicate predicateWithFormat:#"noC >= %#", currMin];
NSPredicate *highNoCPred = [NSPredicate predicateWithFormat:#"noC <= %#", currMax];
NSPredicate *query = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray
arrayWithObjects:dictPred, lowNoCPred, highNoCPred,nil]];
NSLog(#"%#", query);
return query;
}

You can try adding index to your noC and/or dict attributes inside your entity. It might speed up your query time.

Related

How to sort CoreData string attribute in descending order to find the highest current value?

I have a CordData store where the primary "key" is a string; I need to sort that key attribute into descending order so I can find the highest current key. I know I have to convert the key to an int for sorting, but from there I'm lost using comparators (I don't use them often enough to gain any meaningful experience with them). Here is my code so far (which is wrong, I know):
NSArray *sortedArray;
NSString *predicateString = [NSString stringWithFormat:#"sku > %d",0];
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateString];
NSArray *skuRecord = [Books MR_findAllWithPredicate:predicate];
sortedArray = [skuRecord sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
NSString *first = [(Books*)a sku];
NSString *second = [(Books*)b sku];
return [first compare:second];
}];
What's being returned is '1', which is the lowest SKU. I have looked at SO and Google for hours, and found nothing which would help resolve this problem. Can someone please tell me what's wrong with my code? I would appreciate it.
UPDATE: this is the code I finally came up with:
NSString *predicateString = [NSString stringWithFormat:#"sku > %d",0];
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateString];
NSArray *unsortedArray = [Books MR_findAllWithPredicate: predicate];
int highestSKU = 0;
for(int i = 0; i < unsortedArray.count; i++) {
Books *booksManagedObject = (id)unsortedArray[i]; // now in Books array to be able to access .sku
NSString *givenSKU = booksManagedObject.sku;
if([givenSKU intValue] > highestSKU)
highestSKU = [givenSKU intValue];
}
return [NSString stringWithFormat:#"%d",highestSKU];
The usual solution to the problem is to [temporarily] pad the character integer values on the left with zeros, which will cause them to sort correctly even as characters (without converting to integers). Don't know whether this is feasible in your particular instance but I've used that method before.
You can sort using NSSortDescriptors with strings, no need to convert to integers.
NSArray *unsortedArray = #[#"3",#"2",#"5",#"1",#"4"];
NSSortDescriptor *sorter = [NSSortDescriptor sortDescriptorWithKey:#"" ascending:NO];
NSArray *sortedArray = [unsortedArray sortedArrayUsingDescriptors:#[sorter]];
NSLog(#"First = %#, Last = %#",sortedArray[0],[sortedArray lastObject]);
Prints: First = 5, Last = 1
So either I'm missing something in your explanation or vice versa. Did you set ascending to NO?
Your code should look more like this I think;
NSArray *unsortedArray = [Books MR_findAllWithPredicate: predicate];
NSSortDescriptor *sorter = [NSSortDescriptor sortDescriptorWithKey:#"sku" ascending:NO];
NSArray *sortedArray = [unsortedArray sortedArrayUsingDescriptors:#[sorter]];
NSLog(#"First = %#, Last = %#",sortedArray[0],[sortedArray lastObject]);

NSPredicate for NSNumber property of NSManagedObject

I have NSNumber * year property of NSManagedObject, it's type in data model is Integer 16.
I try to check with NSPredicate for this year, but can't find the right one.
What I tried:
NSPredicate *p = nil;
NSNumberFormatter *nf = [[NSNumberFormatter alloc] init];
NSNumber *yearNo = [nf numberFromString:term];
if (yearNo) {
p = [NSPredicate predicateWithFormat:#"(cars.year == %i)", yearNo.intValue];
}
I also tried:
NSPredicate *p = nil;
NSNumberFormatter *nf = [[NSNumberFormatter alloc] init];
NSNumber *yearNo = [nf numberFromString:term];
if (yearNo) {
p = [NSPredicate predicateWithFormat:#"(cars.year == %#)", yearNo];
}
In both cases app crashes.
If you provide more details for your model, we could help you.
But I think the problem is due to cars. If cars is to-many you need a modifier for this
[NSPredicate predicateWithFormat:#"ANY cars.year == %#", yearNo];
As #flexaddicted already said, you have not supplied sufficient information, e.g. for which entity the fetch request is made.
If you want to fetch Car objects with a given year, the predicate is just
[NSPredicate predicateWithFormat:#"year == %#", yearNo]

Show distinct results in fetch request, group by an attribute and calculate the total for that attribute

Scenario:
I have an expense tracking iOS Application and I have a view controller called "DashBoardViewController" (table view controller - with FRC) which would basically categorize my expenses/incomes for a given week, a month, or year and display it as the section header title for example : (Oct 1- Oct 7, 2012) and it shows expenses/incomes ROWS and related stuff according to that particular week or month or year.
My Question:
What I want to accomplish is :
Suppose I save 3 new expenses with SAME category named "Auto" on three different dates(11 nov, 14 nov, 16 nov, 2012 respectively).
In my view controller, I want to display that category "Auto" as a row in table view but it should appear only as ONE ROW and NOT THREE TIMES as I saved three expenses (with category "Auto") and the total amount should be added up for all the 3 expenses I saved (for that particular category). Something like the following screenshot.
I have written some code bit it gives me THREE ROWS for the SAME CATEGORY and not what I actually want (ONE ROW for SAME CATEGORY) and I don't know how I would calculate the total for them? Should be something related to NSPredicate here or fetched results controller.
Any help would be highly appreciated.
- (void)userDidSelectStartDate:(NSDate *)startDate andEndDate:(NSDate *)endDate
{
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Money" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicateDate = [NSPredicate predicateWithFormat:#"(date >= %#) AND (date <= %#)", startDate, endDate];
// Edit the sort key as appropriate.
typeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"type" ascending:YES]; // type refers to an expense or income.
dateSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
if(self.choiceSegmentedControl.selectedIndex == 0) // UISegment Control for "Sorting Category"
{
NSPredicate *predicateCategory = [NSPredicate predicateWithFormat:#"cat == %#", #""];
NSArray * subPredicates = [NSArray arrayWithObjects:predicateCategory, predicateDate, nil];
NSPredicate * compoundPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:subPredicates];
[fetchRequest setPredicate:compoundPredicate];
choiceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"cat" ascending:NO];
}
NSArray * descriptors = [NSArray arrayWithObjects:typeSortDescriptor, dateSortDescriptor, choiceSortDescriptor, nil];
[fetchRequest setSortDescriptors:descriptors];
[fetchRequest setIncludesSubentities:YES];
if(_fetchedResultsController)
{
[_fetchedResultsController release]; _fetchedResultsController = nil;
}
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"type" cacheName:nil];
_fetchedResultsController.delegate = self;
NSError *anyError = nil;
if(![_fetchedResultsController performFetch:&anyError])
{
NSLog(#"error fetching:%#", anyError);
}
__block double totalAmount = 0;
[[self.fetchedResultsController fetchedObjects] enumerateObjectsUsingBlock: ^void (Money *money, NSUInteger idx, BOOL *stop) {
totalAmount += [[money amount] doubleValue];
}];
[fetchRequest release];
//Finally you tell the tableView to reload it's data, it will then ask your NEW FRC for the new data
[self.dashBoardTblView reloadData];
self.startDate = startDate;
self.endDate = endDate;
}
I thought to use NSDictionaryResultType but that's giving a problem with the FRC i have used ( for section names, filling up the table view etc.)
The code where I loop through the FRC gives me the total amount (for income and expenses) BUT I want the total amount for each category (example: total for category "Auto", total for category "Entertainment"). Please help me, I am totally stucked up here.
I don't think you can massage your FRC into returning the kind of objects you need. NSPredicate just filters the kind of objects to return it does not create new ones from the data.
However, you can fetch the your money objects filtered by the date and then calculate the data from the array of money objects using KVC Collection Operators like so:
NSArray *moneyObjectsFilteredbyDate = [self.fetchedResultsController fetchedObjects]
NSArray *categoryStrings = [moneyObjectsFilteredbyDate valueForKeyPath:#"#distinctUnionOfObjects.cat"];
NSArray *sortedCategoryStrings = [categoryStrings sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)];
NSMutableArray *aggregatedDataObjects = [NSMutableArray array];
for (NSString *aCategoryString in sortedCategoryStrings) {
NSPredictate *categoryPredicate = [NSPredicate predicateWithFormat:#"cat == %#", aCategoryString];
NSArray *moneyWithThisCategory = [moneyObjectsFilteredByDate filteredArrayUsingPredicate:categoryPredicate];
NSNumber *sum = [moneyWithThisCategory valueForKeyPath:#"#sum.amount"];
[aggregatedDataObjects addObject:#{#"category" : aCategoryString, #"sum" : sum, #"individualExpenses" : moneyWithThisCategory}];
}
Of course, you could do parts of the in the method where you configure the table cell (like calculating the sum itself), but I hope it gives you an idea. But I don't think you can use the predicate in a form of an SQL query or similar to create new data structure.
Something else you could do: Make the category an individual object of your Core Data model and add a relationship between moneyobjects and Category objects. Then you can just fetch category objects. Although you would then have to filter the expense for a category by the dates.

Multiple fetch requests with complex predicates

I have a couple of views which present data from a CoreData entity in my app. To retrieve the data required for the views I often have to implement multiple fetchRequests which feels wrong - perhaps I'm still making basic mistakes like thinking of CoreData in too much of a SQL database sense.
In fact for one of my views I have 22 fetch requests which may well be the correct way of achieving what I need but as an iPhone/Objective-C novice I can't help questioning my approach. Here is a snippet of my code showing 2 of many fetchRequest, could you give me a nudge in the right direction if I am doing it wrong?
SGK_T4T_01AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityDiscription = [NSEntityDescription entityForName:#"Sessions" inManagedObjectContext:context];
//Swim 3 Count
NSFetchRequest *request2 = [[NSFetchRequest alloc] init];
[request2 setEntity:entityDiscription];
[request2 setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression2 = [NSExpression expressionForKeyPath:#"sport"];
NSExpression *swimCountExpression = [NSExpression expressionForFunction:#"count:" arguments:[NSArray arrayWithObject:keyPathExpression2]];
NSExpressionDescription *expressionDescription2 = [[NSExpressionDescription alloc] init];
[expressionDescription2 setName:#"swimCount"];
[expressionDescription2 setExpression:swimCountExpression];
[expressionDescription2 setExpressionResultType:NSInteger16AttributeType];
[request2 setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription2]];
NSPredicate *pred2 = [NSPredicate predicateWithFormat:#"(date >= %# AND sport like %#)", swimSinceDateAsDate, sportTypeSwim];
[request2 setPredicate:pred2];
NSError *error2;
NSArray *objects2 = [context executeFetchRequest:request2 error:&error2];
if (objects2 == nil) {
NSLog(#"The fetch request returned an array == nil");
} else {
_swimTotalSwimCountLabel.text = [[NSString alloc] initWithFormat:#"%#", [[objects2 objectAtIndex:0] valueForKey:#"swimCount"]];
}
//Swim 4 Fastest 1500m Time Trial
NSFetchRequest *request3 = [[NSFetchRequest alloc] init];
[request3 setEntity:entityDiscription];
[request3 setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression3 = [NSExpression expressionForKeyPath:#"time1"];
NSExpression *swimfastest1500Expression = [NSExpression expressionForFunction:#"min:" arguments:[NSArray arrayWithObject:keyPathExpression3]];
NSExpressionDescription *expressionDescription3 = [[NSExpressionDescription alloc] init];
[expressionDescription3 setName:#"swimFastest1500"];
[expressionDescription3 setExpression:swimfastest1500Expression];
[expressionDescription3 setExpressionResultType:NSInteger16AttributeType];
[request3 setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription3]];
NSString *sessType3 = #"Time Trial - 1500m";
NSPredicate *pred3 = [NSPredicate predicateWithFormat:#"(date >= %# AND sport like %# AND sessiontype like %#)", swimSinceDateAsDate, sportTypeSwim, sessType3];
[request3 setPredicate:pred3];
NSError *error3;
NSArray *objects3 = [context executeFetchRequest:request3 error:&error3];
if (objects3 == nil) {
NSLog(#"The fetch request returned an array == nil");
} else {
NSUInteger durationInSeconds = [[[objects3 objectAtIndex:0] valueForKey:#"swimFastest1500"] integerValue];
NSUInteger durationInMinutes = durationInSeconds / 60;
NSUInteger durationRemainder = durationInSeconds % 60;
_swimTt1500PaceLabel.text = [[NSString alloc] initWithFormat:#"%02i:%02i", durationInMinutes, durationRemainder];
}
Thanks in advance for any assistance, links or direction you may be able to provide...
The predicate stuff doesn't look too bad - but may I ask why you're limiting to all those NSExpressionDescriptions? Did you analyze and find that by limiting to just those properties you found concrete performance improvements? Core Data is usually pretty good at faulting/optimizing your memory, and that might clean up the statements quite a bit.

Number of elements in NSArray where (....)

I am used to working in C# using the linq extensions (list.select(...).where(...) ext), and I was wondering if there was some way of doing the same sort of thing in Objective-C. This would save me from building a number of rather complicated queries using Core Data, which is great for some things, but perhaps not the best for complex queries (or maybe I'm just uninformed).
Is there some kind of equivalent for linq in Objective-C/Core Data?
EDIT: More specifically, I would like to count the number of elements that fit some criteria. Say my model has a field called date. I am trying to select the distinct dates, and then calculate how many of each date there are. In SQL this would be like a group by, and a COUNT aggregate.
Your question goes from very general ("linq equivalent?") to very specific (computing count by date). I'll just answer your specific question.
Unfortunately, NSArray doesn't have a built-in map or select method, but it does offer NSCountedSet, which will compute what you want:
NSCountedSet *dateSet = [NSCountedSet set];
for (id thing in array) {
[dateSet addObject:[thing date]];
}
for (NSDate *date in dateSet) {
NSLog(#"There are %d instances of date %#", [dateSet countForObject:date], date);
}
Change predicate , and "Date" keys with your props
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"child" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:#"Date"]];
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"(start <= %# ) and (completion < 100)",sda ];
[fetchRequest setPredicate:predicate];
int c = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
Here was something posted with comes close
filtering NSArray into a new NSArray in objective-c
Anyway AFAIK you don't have some sort of linq in Objective-C but you have. Arrays and Blocks. And Blocks are functions. So you can really filter on anything in there.
Of course, Cora Data has many functions to make complex queries:
In example to get sum of elements, you have two major ways:
first - get your data to NSSet or NSArray and use #sum operator:
//assume that `childs` are NSArray of your child entities and ammount is attribute to sum, and has attributes start (date) and completion (integer)
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"child" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"(start <= %# ) and (completion < 100)", dzis];
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
NSError *error = nil;
if ([afetchedResultsController performFetch:&error]) {
NSArray *childs = afetchedResultsController.fetchedObjects;
NSNumber *sum=[childs valueForKeyPath:"#sum.ammount"];
}
second is using specific fetch for specific value with added NSExpressionDescription with a sum. This way is harder but better for larger db's
suppose if you have an array of your model objects, you could that with the following statement,
NSArray *distintDatesArray = [array valueForKeyPath:#"#distinctUnionOfObjects.date"];
for ( NSDate *date in distintDatesArray)
{
NSLog (#"Date :%# ,count : %d",date,[[array filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"date = %#",date]] count]);
}
This will have same effect as the group by query.