Hello! Welcome back to our course.
In our last lesson, we focused on application responsiveness by moving long-running Core Data operations to a background thread. This is a crucial step to prevent your UI from freezing. However, responsiveness isn't just about avoiding blocking the main thread; it's also about managing system resources, especially memory.
Today, we'll tackle the performance of our fetch requests when dealing with large datasets. Fetching thousands of objects, even on a background thread, can consume a significant amount of memory and slow down your app. Our learning outcome is to optimize fetch request performance using techniques like setting a fetchBatchSize.
This lesson will equip you with the tools to build scalable, memory-efficient apps that can handle large amounts of data gracefully.
The Problem: The Hidden Cost of Large Fetches
You might think that fetching 10,000 objects is fine as long as it's on a background thread. While Core Data is smart and uses a mechanism called "faulting" (where it doesn't load all of an object's properties until you access them), it still needs to create NSManagedObject instances in memory for every single row returned by the fetch. This process alone can have a substantial memory footprint and take considerable time.
Imagine the following trace from the Instruments profiling tool:

The image clearly shows that fetching a large number of objects at once is a performance bottleneck. Our goal is to avoid this expensive initial fetch.
The Solution: Fetch Batching
The primary solution to this memory problem is fetch batching. Instead of loading all object data at once, we can instruct Core Data to fetch it in smaller, more manageable chunks or "batches."
You accomplish this by setting the fetchBatchSize property on your NSFetchRequest.
When you set this property, the fetch request still returns a single array containing all the managed objects as faults. However, behind the scenes, Core Data only retrieves the data for the first batchSize objects from the persistent store. When you later access an object that is part of a subsequent batch, Core Data automatically and transparently fetches that batch from the store.
A great practical example of this can be found in an article from Hacking with Swift. While the article's main topic is NSFetchedResultsController (which we'll cover in our next lesson), it contains a perfect, self-contained example of setting fetchBatchSize.
Optimizing Core Data Performance using NSFetchedResultsController
Let's read a short section from "Optimizing Core Data Performance" by Paul Hudson to see how fetchBatchSize is implemented.
Please read the code block for the loadSavedData() method. Focus specifically on the line request.fetchBatchSize = 20. Notice how it's a simple property set on the NSFetchRequest instance.
As you saw, applying the optimization is straightforward. Here is how you would apply it to a standard fetch request:
func fetchAllCommits() {
// Assume 'context' is your NSManagedObjectContext
let request = Commit.createFetchRequest()
let sort = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sort]
// The key optimization: Tell Core Data to fetch objects in batches of 25.
request.fetchBatchSize = 25
do {
let commits = try context.fetch(request)
// 'commits' is an array of all commit objects, but most are still faults.
// Core Data has only retrieved the underlying data for the first 25 objects.
// When you access, for example, commits[26], Core Data will automatically
// go to the database and fetch the data for objects 25 through 49.
} catch {
print("Fetch failed: \(error)")
}
}
The ideal fetchBatchSize depends on your UI and data size, but a good starting point is a value slightly larger than the number of items visible on screen at one time.
Beyond Batching: A Deeper Look at Fetch Optimization
fetchBatchSize is your primary tool for managing memory with large datasets, but other techniques are essential for professional-grade performance tuning. The WWDC session on Core Data Best Practices is an excellent resource for these.
Core Data Best Practices - WWDC 2018
This WWDC 2018 session transcript covers several advanced optimization techniques. We'll read a few key sections.
Please read the following parts of the transcript: Batching vs. Limiting: Start at the paragraph beginning "So, now we've got a fetch request...". This section contrasts fetchBatchSize with fetchLimit. SQL Debugging: Jump to the section that begins "com.apple.Core Data.SQLDebug has four levels.". This explains how to enable SQL logging to see what Core Data is doing under the hood. Indexing: Continue to the section that begins "Well, turns out SQLite 3 can actually tell us.". This part shows how to add an index in the Core Data model editor to speed up queries.
Let's summarize and expand on the key points from that reading.
1. Verifying Performance with SQL Debugging
How can you be sure batching is working? You can ask Core Data to show you the SQL it sends to the underlying SQLite database.
To do this, edit your application’s scheme in Xcode, go to the "Arguments" tab, and add this to "Arguments Passed On Launch":-com.apple.CoreData.SQLDebug 1
When you run your app, you will see the SQL queries printed in the console. With fetchBatchSize enabled, you'll see that as you scroll through your data and access objects beyond the initial batch, Core Data will issue new SELECT statements with LIMIT and OFFSET clauses to fetch the next batches on demand. This is concrete proof that you are avoiding a single, massive data load.
2. Capping Results with fetchLimit
As the WWDC transcript mentioned, fetchLimit is different from fetchBatchSize:
fetchBatchSize: Fetches all objects but loads their data in pages to save memory. The final array contains all objects.fetchLimit: Restricts the total number of objects returned by the fetch. This is useful for "Top 10" style queries where you only need a small subset of the total data.
You should always consider setting a fetchBatchSize or fetchLimit for any request that could potentially return an unbounded number of results.
3. Speeding Up Queries with Indexes
If your fetch is slow even with batching, the bottleneck might be the database query itself. This often happens when you are sorting or filtering on a non-indexed property.
As your CS background suggests, a database index is a data structure that improves the speed of data retrieval operations on a database table. In Core Data, you can add indexes directly in the Xcode data model inspector.
If you have a Post entity and you frequently sort posts by timestamp, you should add an index for the timestamp attribute. As shown in the WWDC resource, you can select the entity, go to the Data Model Inspector (right-hand pane), and add a "Fetch Index". This can change a query from a full table scan (slow) to an index seek (fast), dramatically improving performance.
Conclusion
You've now learned how to move beyond basic fetches to write highly performant, memory-efficient queries that can scale to very large datasets.
Key Takeaways:
- Fetching thousands of Core Data objects at once is a performance anti-pattern due to memory overhead, even with faulting.
- Use
fetchBatchSizeonNSFetchRequestto load data in pages, which is the primary technique for managing memory with large result sets. - Use
fetchLimitwhen you only need a fixed number of objects from a potentially large set. - Enable
com.apple.CoreData.SQLDebugto inspect the generated SQL and verify that your optimizations are working as expected. - Add indexes to your Core Data model on attributes that you frequently use for sorting or in predicates to accelerate the database lookup itself.
Preview of the Next Lesson:
We've made our fetches memory-efficient. Now, how do we efficiently connect a large, batched dataset to a UITableView and automatically handle UI updates when data changes? In our next lesson, we will learn to use NSFetchedResultsController to efficiently display Core Data objects in a UITableView. This powerful class seamlessly integrates fetch batching with table view data source management.