Fetch Requests and Querying
Fetch Requests and Querying
Section titled “Fetch Requests and Querying”Optimizing fetch requests is crucial for app performance. This guide covers best practices for querying Core Data efficiently, from basic fetches to advanced aggregations.
Basic Fetch Request
Section titled “Basic Fetch Request”let fetchRequest: NSFetchRequest<Article> = Article.fetchRequest()let articles = try context.fetch(fetchRequest)Optimization Strategies
Section titled “Optimization Strategies”1. Limit Properties Fetched
Section titled “1. Limit Properties Fetched”Only fetch the properties you actually need:
let fetchRequest = Article.fetchRequest()fetchRequest.propertiesToFetch = ["name", "creationDate"]
// For list view, you might only need:fetchRequest.propertiesToFetch = ["name", "categoryName", "views"]SQL Impact:
-- Without propertiesToFetchSELECT * FROM ZARTICLE
-- With propertiesToFetchSELECT Z_PK, ZNAME, ZCREATIONDATE FROM ZARTICLEBenefits:
- Reduces memory usage
- Faster query execution
- Less data transferred from disk
2. Use Batch Fetching
Section titled “2. Use Batch Fetching”Fetch objects in batches to avoid loading everything at once:
let fetchRequest = Article.fetchRequest()fetchRequest.fetchBatchSize = 20How it works:
- Initially fetches only 20 objects
- Fetches next batch when needed (scrolling, iteration)
- Keeps memory usage predictable
When to use:
- List views (table/collection views)
- Large datasets
- Scrollable content
3. Set Fetch Limit
Section titled “3. Set Fetch Limit”When you only need a specific number of results:
let fetchRequest = Article.fetchRequest()fetchRequest.fetchLimit = 1 // Only fetch one resultCommon use cases:
// Get newest articlefetchRequest.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]fetchRequest.fetchLimit = 1
// Get top 10 most viewedfetchRequest.sortDescriptors = [NSSortDescriptor(key: "views", ascending: false)]fetchRequest.fetchLimit = 104. Fetch Only Object IDs
Section titled “4. Fetch Only Object IDs”For counting or checking existence, fetch only IDs:
let fetchRequest = Article.fetchRequest()fetchRequest.resultType = .managedObjectIDResultType
let objectIDs = try context.fetch(fetchRequest) as! [NSManagedObjectID]Benefits:
- Minimal memory usage
- Very fast
- No faulting overhead
Use for:
- Counting objects
- Checking existence
- Batch operations
- Validation
Sort Descriptors
Section titled “Sort Descriptors”Always specify sort descriptors for predictable results:
let fetchRequest = Article.fetchRequest()fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false)]Multiple Sort Descriptors
Section titled “Multiple Sort Descriptors”fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "category.name", ascending: true), NSSortDescriptor(key: "name", ascending: true)]Case-Insensitive Sorting
Section titled “Case-Insensitive Sorting”let sortDescriptor = NSSortDescriptor( key: "name", ascending: true, selector: #selector(NSString.caseInsensitiveCompare(_:)))fetchRequest.sortDescriptors = [sortDescriptor]Localized Sorting
Section titled “Localized Sorting”let sortDescriptor = NSSortDescriptor( key: "name", ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))fetchRequest.sortDescriptors = [sortDescriptor]Predicates
Section titled “Predicates”Filter results using predicates:
Basic Predicates
Section titled “Basic Predicates”// Exact matchfetchRequest.predicate = NSPredicate(format: "name == %@", "SwiftLee")
// ContainsfetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", "swift")// [c] = case insensitive, [d] = diacritic insensitive
// Begins withfetchRequest.predicate = NSPredicate(format: "name BEGINSWITH[c] %@", "Swift")
// Greater thanfetchRequest.predicate = NSPredicate(format: "views > %d", 100)
// Date rangelet startDate = Calendar.current.startOfDay(for: Date())let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!fetchRequest.predicate = NSPredicate( format: "creationDate >= %@ AND creationDate < %@", startDate as NSDate, endDate as NSDate)Compound Predicates
Section titled “Compound Predicates”// ANDlet predicate1 = NSPredicate(format: "views > %d", 100)let predicate2 = NSPredicate(format: "category.name == %@", "Swift")fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate1, predicate2])
// ORfetchRequest.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: [predicate1, predicate2])
// NOTfetchRequest.predicate = NSCompoundPredicate(notPredicateWithSubpredicate: predicate1)Relationship Predicates
Section titled “Relationship Predicates”// Articles with a specific categoryfetchRequest.predicate = NSPredicate(format: "category.name == %@", "Swift")
// Articles with any attachmentsfetchRequest.predicate = NSPredicate(format: "attachments.@count > 0")
// Articles with more than 5 attachmentsfetchRequest.predicate = NSPredicate(format: "attachments.@count > 5")
// Using ANYfetchRequest.predicate = NSPredicate(format: "ANY attachments.size > %d", 1000000)
// Using ALLfetchRequest.predicate = NSPredicate(format: "ALL attachments.isDownloaded == YES")IN Predicate
Section titled “IN Predicate”let names = ["Swift", "iOS", "Core Data"]fetchRequest.predicate = NSPredicate(format: "name IN %@", names)NSFetchedResultsController
Section titled “NSFetchedResultsController”For table and collection views, use NSFetchedResultsController for automatic updates:
class ArticlesViewController: UIViewController { var fetchedResultsController: NSFetchedResultsController<Article>!
func setupFetchedResultsController() { let fetchRequest = Article.fetchRequest() fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ] fetchRequest.fetchBatchSize = 20
fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: "ArticlesCache" )
fetchedResultsController.delegate = self
try? fetchedResultsController.performFetch() }}With Sections
Section titled “With Sections”fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: "category.name", // Group by category cacheName: "ArticlesByCategoryCache")Delegate Methods (UITableView)
Section titled “Delegate Methods (UITableView)”extension ArticlesViewController: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.beginUpdates() }
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert: if let indexPath = newIndexPath { tableView.insertRows(at: [indexPath], with: .automatic) } case .delete: if let indexPath = indexPath { tableView.deleteRows(at: [indexPath], with: .automatic) } case .update: if let indexPath = indexPath { tableView.reloadRows(at: [indexPath], with: .automatic) } case .move: if let indexPath = indexPath, let newIndexPath = newIndexPath { tableView.deleteRows(at: [indexPath], with: .automatic) tableView.insertRows(at: [newIndexPath], with: .automatic) } @unknown default: break } }
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.endUpdates() }}Diffable Data Sources (iOS 13+)
Section titled “Diffable Data Sources (iOS 13+)”Modern approach using NSDiffableDataSourceSnapshot:
class ArticlesViewController: UICollectionViewController { private var dataSource: UICollectionViewDiffableDataSource<String, NSManagedObjectID>! private var fetchedResultsController: NSFetchedResultsController<Article>!
func setupDataSource() { dataSource = UICollectionViewDiffableDataSource<String, NSManagedObjectID>( collectionView: collectionView ) { collectionView, indexPath, objectID in let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "ArticleCell", for: indexPath ) as! ArticleCell
if let article = try? self.viewContext.existingObject(with: objectID) as? Article { cell.configure(with: article) }
return cell } }
func setupFetchedResultsController() { let fetchRequest = Article.fetchRequest() fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil )
fetchedResultsController.delegate = self try? fetchedResultsController.performFetch() }}
extension ArticlesViewController: NSFetchedResultsControllerDelegate { func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID> dataSource.apply(snapshot, animatingDifferences: true) }}Aggregate Fetching with NSExpression
Section titled “Aggregate Fetching with NSExpression”For statistics and aggregations:
// Simple countlet count = try context.count(for: Article.fetchRequest())
// Count with predicatelet fetchRequest = Article.fetchRequest()fetchRequest.predicate = NSPredicate(format: "views > %d", 100)let count = try context.count(for: fetchRequest)Sum, Average, Min, Max
Section titled “Sum, Average, Min, Max”let fetchRequest = Article.fetchRequest()fetchRequest.resultType = .dictionaryResultType
// Sum of viewslet sumExpression = NSExpression(format: "@sum.views")let sumDescription = NSExpressionDescription()sumDescription.name = "totalViews"sumDescription.expression = sumExpressionsumDescription.expressionResultType = .integer64AttributeType
fetchRequest.propertiesToFetch = [sumDescription]
let results = try context.fetch(fetchRequest) as! [[String: Any]]if let totalViews = results.first?["totalViews"] as? Int { print("Total views: \(totalViews)")}Group By with Aggregates
Section titled “Group By with Aggregates”let fetchRequest = Article.fetchRequest()fetchRequest.resultType = .dictionaryResultType
// Category namelet categoryExpression = NSExpression(forKeyPath: "category.name")let categoryDescription = NSExpressionDescription()categoryDescription.name = "categoryName"categoryDescription.expression = categoryExpressioncategoryDescription.expressionResultType = .stringAttributeType
// Sum of views per categorylet sumExpression = NSExpression(format: "@sum.views")let sumDescription = NSExpressionDescription()sumDescription.name = "totalViews"sumDescription.expression = sumExpressionsumDescription.expressionResultType = .integer64AttributeType
fetchRequest.propertiesToFetch = [categoryDescription, sumDescription]fetchRequest.propertiesToGroupBy = ["category.name"]fetchRequest.sortDescriptors = [NSSortDescriptor(key: "categoryName", ascending: true)]
let results = try context.fetch(fetchRequest) as! [[String: Any]]for result in results { let category = result["categoryName"] as? String ?? "Unknown" let views = result["totalViews"] as? Int ?? 0 print("\(category): \(views) views")}Count Per Group
Section titled “Count Per Group”let fetchRequest = Article.fetchRequest()fetchRequest.resultType = .dictionaryResultType
let categoryExpression = NSExpression(forKeyPath: "category.name")let categoryDescription = NSExpressionDescription()categoryDescription.name = "categoryName"categoryDescription.expression = categoryExpressioncategoryDescription.expressionResultType = .stringAttributeType
let countExpression = NSExpression(forFunction: "count:", arguments: [NSExpression(forKeyPath: "objectID")])let countDescription = NSExpressionDescription()countDescription.name = "count"countDescription.expression = countExpressioncountDescription.expressionResultType = .integer64AttributeType
fetchRequest.propertiesToFetch = [categoryDescription, countDescription]fetchRequest.propertiesToGroupBy = ["category.name"]
let results = try context.fetch(fetchRequest) as! [[String: Any]]Typed Fetch Requests with Managed Protocol
Section titled “Typed Fetch Requests with Managed Protocol”Create a protocol for type-safe fetch requests:
protocol Managed: NSManagedObject { static var entityName: String { get }}
extension Managed { static var entityName: String { return String(describing: self) }
static func fetchRequest<T: NSManagedObject>() -> NSFetchRequest<T> { return NSFetchRequest<T>(entityName: entityName) }}
// Conform your entitiesextension Article: Managed {}
// Usagelet fetchRequest: NSFetchRequest<Article> = Article.fetchRequest()Asynchronous Fetching
Section titled “Asynchronous Fetching”For large datasets, fetch asynchronously:
let fetchRequest = Article.fetchRequest()let asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { result in guard let articles = result.finalResult else { return }
DispatchQueue.main.async { // Update UI with articles }}
try? context.execute(asyncFetchRequest)Faulting Control
Section titled “Faulting Control”Prefetching Relationships
Section titled “Prefetching Relationships”let fetchRequest = Article.fetchRequest()fetchRequest.relationshipKeyPathsForPrefetching = ["category", "attachments"]Benefits:
- Reduces number of database trips
- Improves performance when accessing relationships
- Prevents N+1 query problem
Returning Faults
Section titled “Returning Faults”fetchRequest.returnsObjectsAsFaults = falseWhen to use:
- You know you’ll access all properties immediately
- Small result sets
- Avoid for large datasets (high memory usage)
Common Patterns
Section titled “Common Patterns”Fetch Single Object by ID
Section titled “Fetch Single Object by ID”func fetchArticle(withID id: NSManagedObjectID) -> Article? { return try? context.existingObject(with: id) as? Article}Fetch or Create
Section titled “Fetch or Create”func fetchOrCreateArticle(withName name: String) -> Article { let fetchRequest = Article.fetchRequest() fetchRequest.predicate = NSPredicate(format: "name == %@", name) fetchRequest.fetchLimit = 1
if let existing = try? context.fetch(fetchRequest).first { return existing }
let article = Article(context: context) article.name = name return article}Check Existence
Section titled “Check Existence”func articleExists(withName name: String) -> Bool { let fetchRequest = Article.fetchRequest() fetchRequest.predicate = NSPredicate(format: "name == %@", name) fetchRequest.fetchLimit = 1 fetchRequest.resultType = .countResultType
let count = (try? context.count(for: fetchRequest)) ?? 0 return count > 0}Performance Tips
Section titled “Performance Tips”❌ Don’t Fetch Everything
Section titled “❌ Don’t Fetch Everything”// Bad: Fetches all properties, all objectslet articles = try context.fetch(Article.fetchRequest())let count = articles.count✅ Use Count Request
Section titled “✅ Use Count Request”// Good: Only counts, doesn't fetch objectslet count = try context.count(for: Article.fetchRequest())❌ Don’t Access Relationships in Loops
Section titled “❌ Don’t Access Relationships in Loops”// Bad: Fires fault for each articlefor article in articles { print(article.category?.name) // Fault!}✅ Prefetch Relationships
Section titled “✅ Prefetch Relationships”// Good: Prefetches all categories at oncelet fetchRequest = Article.fetchRequest()fetchRequest.relationshipKeyPathsForPrefetching = ["category"]let articles = try context.fetch(fetchRequest)
for article in articles { print(article.category?.name) // No fault!}❌ Don’t Fetch in Loops
Section titled “❌ Don’t Fetch in Loops”// Bad: Multiple fetch requestsfor name in names { let fetchRequest = Article.fetchRequest() fetchRequest.predicate = NSPredicate(format: "name == %@", name) let articles = try? context.fetch(fetchRequest)}✅ Use IN Predicate
Section titled “✅ Use IN Predicate”// Good: Single fetch requestlet fetchRequest = Article.fetchRequest()fetchRequest.predicate = NSPredicate(format: "name IN %@", names)let articles = try context.fetch(fetchRequest)Debugging Fetch Requests
Section titled “Debugging Fetch Requests”Enable SQL Debug
Section titled “Enable SQL Debug”Add launch argument:
-com.apple.CoreData.SQLDebug 1Output:
CoreData: sql: SELECT Z_PK, ZNAME, ZVIEWS FROM ZARTICLE WHERE ZVIEWS > ? ORDER BY ZCREATIONDATE DESC LIMIT 20Measure Fetch Performance
Section titled “Measure Fetch Performance”let startTime = CFAbsoluteTimeGetCurrent()let articles = try context.fetch(fetchRequest)let timeElapsed = CFAbsoluteTimeGetCurrent() - startTimeprint("Fetch took \(timeElapsed) seconds")Summary
Section titled “Summary”- Use
propertiesToFetchto limit fetched properties - Set
fetchBatchSizefor large datasets (typically 20-50) - Use
fetchLimitwhen you only need a few results - Always specify sort descriptors for predictable results
- Use predicates to filter at the database level
- Use
NSFetchedResultsControllerfor list views - Prefetch relationships to avoid N+1 queries
- Use count requests instead of fetching for counts
- Use aggregate expressions for statistics
- Enable SQL debug to understand query performance