[SalesForce] Avoid ‘Apex CPU time limit exceeded’ with Batch Process

I encountered this error and have trouble with performance tuning.

I tried asyncronous approach, but Apex CPU time limit exceeded occurs continuously.
Anybody can make it more efficient?

Here is code and size of each Lists from static method is about 15,000.
Please help.

static List<Opportunity> getOppList(){
    return [SELECT ID, StageName, SuccessDate__c FROM Opportunity];
}

static List<Schedule__c> getScheduleList(){
    return [SELECT ID, Opportunity__c, StartTime__c FROM Schedule__c WHERE Result__c = 'Complete'ORDER BY Opportunity__c];
}

global Database.QueryLocator start(Database.BatchableContext BC) {
    query = 'SELECT ID FROM CASE';
    return Database.getQueryLocator(query);
}

global void execute(Database.BatchableContext BC, List<sObject> scope) {
    List<Opportunity> allopps = getOppList();
    List<Schedule__c> schedules = getScheduleList();

    List<Schedule__c> filtered = new List<Schedule__c>();
    Datetime dt;
    Boolean check;
    Boolean status;

    for ( Opportunity opp : allopps ){
        dt = null;
        check = false;
        status = false;
        for (Schedule__c schedule : schedules ){    
            if( check == false && status == true )
                break;

            if( schedule.Opportunity__c == opp.id ){
                check = true;
                filtered.add(schedule);
                if( dt == null || schedule.StartTime__c < dt)
                    dt = schedule.StartTime__c;

                i++;
            }
            else if( check == true && status == false){
                check = false;
                status = true;
            }
        }

        if( filtered.size() >= 1){
            opp.SuccessDate__c = dt;
            opp.StageName = 'Success';
        }
        else if( filtered.size() == 0 && opp.StageName == 'Success'){
            opp.SuccessDate__c = null;
            opp.StageName = 'Initial Stage';
        }
    }

    System.debug('schedule size : ' + schedules.size());
    System.debug('size : ' + allopps.size() + ' count : ' + i);
    /*
    try{
        update allopps;
    }catch( Exception e){
        System.debug('Error : ' + e.getMessage());
    }
    */
}

Best Answer

You need to rework how the batch is processing the records.

Firstly, base your batch getQueryLocator on the either the Opportunity or Schedule__c records. There doesn't appear to be any reason to bring Case records in here as you don't use the scope for anything.

Next, limit the second query to just those records applicable to the first. So, say your query locator is running through all the Opportunity records. You could then have the SOQL query for Schedule__c records look for only the opportunity records that in in scope.


A very rough example:

public Database.QueryLocator start(Database.BatchableContext BC) {
    query = 'SELECT ID, StageName, SuccessDate__c FROM Opportunity';
    return Database.getQueryLocator(query);
}

public void execute(Database.BatchableContext BC, List<Opportunity> scope) {
    Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>(scope);

    List<Schedule__c> schedules = ;

    // Build up a map form the Schedule__c.Opportunity__c to the Schedule__c records
    Map<Id, List<Schedule__c>> opportunityIdToScheduleMap = new Map<Id, List<Schedule__c>>();
    for(Schedule__c schedule : [SELECT ID, Opportunity__c, StartTime__c FROM Schedule__c WHERE Result__c = 'Complete' and Opportunity__c in :oppMap.keySet() ORDER BY Opportunity__c]) {
    {
        Id oppId = schedule.Opportunity__c;
        List<Schedule__c> schedulesForOpp = null;
        if(opportunityIdToScheduleMap.containsKey(oppId)) {
            // Could check for  schedule.StartTime__c < dt here and just keep one.
            schedulesForOpp = opportunityIdToScheduleMap.get(oppId);
        } else {
            schedulesForOpp = new List<Schedule__c>();
            opportunityIdToScheduleMap.put(oppId, schedulesForOpp);
        }
        schedulesForOpp.add(schedule);
    }

    List<Opportunity> toUpdate = new List<Opportunity>();

    // Loop through the Opportunities in scope
    for(Opportunity opp : scope) {
        // Checking the schedule map to see if they are scheduled (or not)
        if(opportunityIdToScheduleMap.containsKey(opp.Id)) {
            List<Schedule__c> schedulesForOpp = opportunityIdToScheduleMap.containsKey(opp.Id);
            // Loop over records to find the lowest  schedule.StartTime__c < dt
            // ...

            // I've just grabbed the first. Finding the lowest startTime from the list should be a quick job.
            opp.SuccessDate__c = schedulesForOpp[0].StartTime__c;
            opp.StageName = 'Success';
            toUpdate.add(opp);
        } else if (opp.StageName == 'Success') {
            opp.SuccessDate__c = null;
            opp.StageName = 'Initial Stage';
            toUpdate.add(opp);
        }

    }

    // Update the scoped Opportunities as required
    update toUpdate;
}

You could probably get smarter with the SOQL queries, but this should generally scale much better.


Also, another massive benefit of batches is the ability to define how big the scope should be. This can be done with the optional scope parameter on Database.executeBatch();

Related Topic