Core Data slow to save

iOS 18.2, Swift, Xcode 16.2

I have a Core Data model with two entities - WarehouseArea (of which there is only one object) and StockReeipt (of which there are a couple of hundred thousand). Each StockReceipt must be linked to a WarehouseArea, and a WarehouseArea can be linked to zero, one or many StockReceipts.

My problem is that when I create and add one more StockReceipt, the Core Data save takes over 3 seconds to complete. I don't understand why this is so slow. Saving the initial 200,000 StockReceipts only takes 5-6 seconds.

When I enable SQL logging I can see that when the WarehouseArea attribute is being set on a StockReceipt, Core Data fetches all of the other StockReceipts (I don't know why) but that only takes 0.2 seconds and none of those StockReceipts are modified, so there shouldn't be any need to process them when saving the context.

I have prepared a test project which can be found at https://github.com/DaleReilly/CoreDataSaveTester . Running the project will produce NSLog output showing the times before and after the slow save.

Please help me understand what is going on in the background and tell me if there is any way I can speed this up?

Answered by DTS Engineer in 822552022

From your project, I see that you are using a nested context:

let pscMoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
pscMoc.persistentStoreCoordinator = persistentStoreCoordinator

let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.parent = pscMoc

Is there any strong reason that you need a parent context in between your main context and the persistent store coordinator? If not, I'd suggest that you avoid doing that, because to support a nested context, Core Data needs to do more work to coordinate the data saving and fetching:

//moc.parent = pscMoc
moc.persistentStoreCoordinator = persistentStoreCoordinator

With this simple change, you get a significantly better performance, as shown in the following log:

  • Before (around 2.55s / 1.05s)
Saving 200,000 receipts at 2025-01-24 16:14:06.3430
Finished saving 200,000 receipts at 2025-01-24 16:14:08.8920
Saving one more receipt at 2025-01-24 16:14:08.8970
Finished saving one receipt at 2025-01-24 16:14:09.9480
  • After (around 1.08s / 0.03s)
Saving 200,000 receipts at 2025-01-24 16:14:51.6010
Finished saving 200,000 receipts at 2025-01-24 16:14:52.6870
Saving one more receipt at 2025-01-24 16:14:52.6920
Finished saving one receipt at 2025-01-24 16:14:52.7200

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

From your project, I see that you are using a nested context:

let pscMoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
pscMoc.persistentStoreCoordinator = persistentStoreCoordinator

let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.parent = pscMoc

Is there any strong reason that you need a parent context in between your main context and the persistent store coordinator? If not, I'd suggest that you avoid doing that, because to support a nested context, Core Data needs to do more work to coordinate the data saving and fetching:

//moc.parent = pscMoc
moc.persistentStoreCoordinator = persistentStoreCoordinator

With this simple change, you get a significantly better performance, as shown in the following log:

  • Before (around 2.55s / 1.05s)
Saving 200,000 receipts at 2025-01-24 16:14:06.3430
Finished saving 200,000 receipts at 2025-01-24 16:14:08.8920
Saving one more receipt at 2025-01-24 16:14:08.8970
Finished saving one receipt at 2025-01-24 16:14:09.9480
  • After (around 1.08s / 0.03s)
Saving 200,000 receipts at 2025-01-24 16:14:51.6010
Finished saving 200,000 receipts at 2025-01-24 16:14:52.6870
Saving one more receipt at 2025-01-24 16:14:52.6920
Finished saving one receipt at 2025-01-24 16:14:52.7200

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thank you for that suggestion.

I am following advice I found in an online series about the best way to set up a Core Data stack. It recommended using a private managed object context to perform background saves to the persistent store, and having a child context linked to the main queue to handle operations relating to the UI. This advice was justified on the basis that saves to the persistent store could take a while, and shouldn't block the main queue.

However, in my situation it seems that the main queue save is taking considerably longer than the private queue one. I should also say that in my real project, I am also creating child contexts of the main context from time to time which run on background queues to avoid blocking the main queue, so I don’t think I can do everything on a single context linked directly to the persistent store coordinator.

Is there anything I can do to speed up the processing itself, other than avoiding using a relationship in the data model?

Not that I am aware of. Again, if you are using a nested context and it leads to performance issues, my opinion wil be that you avoid using it.

If you have a heavy task that you don't want to do in the main queue, use a background context to do that and merge the relevant changes to the main context, as shown in the Loading and Displaying a Large Data Feed sample.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Core Data slow to save
 
 
Q