Search code examples
iosswiftcore-data

CoreData : Why updating single column using Batch Update Operation is much slower than using NSManagedObject?


All the while, my thoughts on Batch Update Operations are that they are much faster than updating via NSManagedObject. However, as per my testing on a 2000 rows production data set, using Batch Update Operations is 2 to 3 times slower than updating via NSManagedObject.

My update pattern is, there are around 2000 rows. I am updating each of their column named "order", with different value for each row.

The goal is simple, we want to make the current row's order value, is larger than previous row's order value.

Here's my code snippet. nsPlainNote is an NSManagedObject

Update via Batch Update Operation (slow)

private func _updateOrdersIfPossible2(context: NSManagedObjectContext, updateOrders: [UpdateOrder]) {
    let count = updateOrders.count
    
    if count < 2 {
        return
    }
    
    var prevOrder = updateOrders[0].order
    
    var updatedObjectIDs = [NSManagedObjectID]()
    
    if !Utils.isValidOrder(prevOrder) {
        prevOrder = prevOrder - 1
        
        precondition(Utils.isValidOrder(prevOrder))
        
        // Time-consuming operation.
        if let updatedObjectID = _updateWithoutMerge(context: context, objectID: updateOrders[0].objectID, propertiesToUpdate: [
            "order": prevOrder
        ]) {
            updatedObjectIDs.append(updatedObjectID)
        }
    }
    
    for index in 1..<count {
        precondition(Utils.isValidOrder(prevOrder))
        
        let updateOrder = updateOrders[index]
        
        if !Utils.isValidOrder(updateOrder.order) || updateOrder.order <= prevOrder {
            var newOrder = prevOrder + 1
            
            if !Utils.isValidOrder(newOrder) {
                newOrder = newOrder + 1
            }
            
            precondition(newOrder > prevOrder)
            
            prevOrder = newOrder
            
            precondition(Utils.isValidOrder(newOrder))
            
            // Time-consuming operation.
            if let updatedObjectID = _updateWithoutMerge(context: context, objectID: updateOrder.objectID, propertiesToUpdate: [
                "order": newOrder
            ]) {
                updatedObjectIDs.append(updatedObjectID)
            }
        } else {
            // Skip from updating. Fast!
            
            prevOrder = updateOrder.order
        }
    }   // for index in 1..<count
    
    if !updatedObjectIDs.isEmpty {
        let changes = [NSUpdatedObjectsKey : updatedObjectIDs]
        CoreDataStack.INSTANCE.mergeChanges(changes)
    }
}

private func _updateWithoutMerge(context: NSManagedObjectContext, objectID: NSManagedObjectID, propertiesToUpdate: [AnyHashable : Any]) -> NSManagedObjectID? {
    return RepositoryUtils._updateWithoutMerge(
        context: context,
        entityName: "NSPlainNote",
        objectID: objectID,
        propertiesToUpdate: propertiesToUpdate
    )
}

static func _updateWithoutMerge(context: NSManagedObjectContext, entityName: String, objectID: NSManagedObjectID, propertiesToUpdate: [AnyHashable : Any]) -> NSManagedObjectID? {
    var result: NSManagedObjectID? = nil
    
    do {
        let batchUpdateRequest = NSBatchUpdateRequest(entityName: entityName)
        batchUpdateRequest.predicate = NSPredicate(format: "self = %@", objectID)
        batchUpdateRequest.propertiesToUpdate = propertiesToUpdate
        batchUpdateRequest.resultType = .updatedObjectIDsResultType
        
        let batchUpdateResult = try context.execute(batchUpdateRequest) as? NSBatchUpdateResult
        
        if let managedObjectIDs = batchUpdateResult?.result as? [NSManagedObjectID] {
            result = managedObjectIDs.first
        }
    } catch {
        context.rollback()
        
        error_log(error)
    }
    
    return result
}

As per my benchmark, most time are spent in the loop of calling _updateWithoutMerge.


Update via NSManagedObject (Faster)

private func _updateOrdersIfPossible(context: NSManagedObjectContext, updateOrders: [UpdateOrder]) {
    let count = updateOrders.count
    if count < 2 {
        return
    }
    var prevOrder = updateOrders[0].order
    var updatedObjectIDs = [NSManagedObjectID]()
    if !Utils.isValidOrder(prevOrder) {
        prevOrder = prevOrder - 1
        precondition(Utils.isValidOrder(prevOrder))
        // Time-consuming operation.
        if let nsPlainNote = NSPlainNoteRepository.getNSPlainNote(context: context, objectID: updateOrders[0].objectID, propertiesToFetch: ["order"]) {
            nsPlainNote.order = prevOrder
        }
    }
    for index in 1..<count {
        precondition(Utils.isValidOrder(prevOrder))
        let updateOrder = updateOrders[index]
        if !Utils.isValidOrder(updateOrder.order) || updateOrder.order <= prevOrder {
            var newOrder = prevOrder + 1
            if !Utils.isValidOrder(newOrder) {
                newOrder = newOrder + 1
            }
            precondition(newOrder > prevOrder)
            prevOrder = newOrder
            precondition(Utils.isValidOrder(newOrder))
            // Time-consuming operation.
            if let nsPlainNote = NSPlainNoteRepository.getNSPlainNote(context: context, objectID: updateOrder.objectID, propertiesToFetch: ["order"]) {
                nsPlainNote.order = newOrder
            }
        } else {
            // Skip from updating. Fast!
            prevOrder = updateOrder.order
        }
    }   // for index in 1..<count
    RepositoryUtils.saveContextIfPossible(context)
}
static func getNSPlainNote(context: NSManagedObjectContext, objectID: NSManagedObjectID, propertiesToFetch: [Any]?) -> NSPlainNote? {
    var nsPlainNote: NSPlainNote? = nil
    context.performAndWait {
        let fetchRequest = NSPlainNote.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "self = %@", objectID)
        fetchRequest.propertiesToFetch = propertiesToFetch
        fetchRequest.fetchLimit = 1
        do {
            let nsPlainNotes = try fetchRequest.execute()
            if let _nsPlainNote = nsPlainNotes.first {
                nsPlainNote = _nsPlainNote
            }
        } catch {
            error_log(error)
        }
    }
    return nsPlainNote
}

I thought using Batch Update Operation should be faster than using NSManagedObject?

Am I having wrong expectation, or there is an error in my implementation?

Thanks.


Solution

  • I do not see how you are really using a batch update in your code. You create a new NSBatchUpdateRequest with a predicate for every single objectID, defeating the purpose of it being a batch update.

    Also, a batch update's purpose is to change a field for all objects matching a criteria to the same new value. If you want to bump each respective order one by one, it means it has to load each object's value, increase it, and then save it again. NSBatchUpdateRequest is not the correct tool for that purpose.