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());
    }
    */
}
  • Is Opportunity__c a lookup field on Schedule__c object? – kurunve Oct 11 at 8:24
  • @kurunve Yes, right. – YoungHoon Kim Oct 11 at 8:25

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();

  • Hi @Daniel Ballinger, getQueryLocator is meaningless in my code. I use 2 static method for getting Lists. Is there any differences between using getQueryLocator and using static method for getting List? and I don't know how can I pass 2 Lists from getQueryLocator to execute. – YoungHoon Kim Oct 11 at 8:26
  • The getQueryLocator is the whole reason for batch jobs existing in Apex. It allows Salesforce to take what would be a massive/overwhelming result set (all the Opportunity records in your org) and instead give you smaller manageable blocks to work with via the scope. This allows you to avoid all sorts of limits that occur when trying to process large amounts of data. – Daniel Ballinger Oct 11 at 8:32
  • You don't need to use two SOQL queries up from. You can use the scope for the results from the first query to run a second much smaller query in the execute method. – Daniel Ballinger Oct 11 at 8:33
  • So, the main reason this process make over CPU hitting is List X List logic. If I locate first Opportunity list in getQueryLocator, and loop each scope list, I need List X List action too. Could you please guide me or modify above code a little conceptually? – YoungHoon Kim Oct 11 at 8:37
  • Looks like I couldn't understand your guide perfectly. – YoungHoon Kim Oct 11 at 8:38

First Understand, Why you are getting 'Apex CPU time limit exceeded' with Batch Process.

  1. Salesforce has a timeout limit for transactions based on CPU usage. If transactions consume too much CPU time, we'll shut them down as a long-running transaction.

Salesforce counts almost everything else that happens on the app server, including declarative actions. If DML in your code encounters a validation rule with a formula, we'll count the time spent evaluating that formula. CPU time is calculated for all executions on the Salesforce application servers occurring in one Apex transaction—for the executing Apex code, and any processes that are called from this code, such as package code and workflows. CPU time is private for a transaction and is isolated from other transactions. Operations that don’t consume application server CPU time aren’t counted toward CPU time.

  1. What's counted

    • All Apex code
    • Library functions exposed in Apex
    • Workflow execution
  2. What's not counted

    • Database operations, e.g. DML, SOQL
    • SOSL
    • HTTP callouts

You will get more Information here:-Code more efficiently to avoid 'Apex CPU time limit exceeded'

In Your Case, getOppList and getScheduleList method being called for every scope. Suppose that after running the class, total batch is 1000, so these two methods will be called 1000 times. You can avoid it.

Second, You are using nested for loops, You should avoid it. As I can see, For each scope you are doing nested for loop. let say, your getOppList returns 40000 records and getScheduleList returns 20000 records and your total number of the batch is 1000. Every scope you are executing logic 40000 x 20000 times.

@DanielBallinger has suggested the optimized code, so go through it.

Note:- Database.getQueryLocator will return you 5000000 records at a time, while normal query will return you 50000 only.

As @Daniel suggested, you will have to rework the batch.

  1. Use the start method to query records that you will be processing in the execute method. (In your case the query in the start method should either be on Opportunity or on Schedule__c)
  2. Try to avoid usage of for loop inside for loop unless it is actually needed.

Hope this helps.

global Database.QueryLocator start(Database.BatchableContext BC) {
    query = 'SELECT ID, Opportunity__c, StartTime__c FROM Schedule__c WHERE Result__c = \'Complete\'';
    return Database.getQueryLocator(query);
}

global void execute(Database.BatchableContext BC, List<Schedule__c> scope) {
    List<Opportunity> opportunityList = new List<Opportunity>();
    Set<Id> opportunityIdSet = new Set<Id>();

    //Fetch the opportunity__c value from the scope into a set.
    for(Schedule__c scheduleObj : scope){
        opportunityIdSet.add(scheduleObj.Opportunity__c);
    }

    //Query Opportunities where the Id is IN opportunityIdSet
    opportunityList = [SELECT Id FROM Opportunity WHERE Id IN :opportunityIdSet];

    if(!opportunityList.isEmpty()){
        // Iterate over the opportunityList
    }
}

Visit the salesforce documentation for detailed information about using batch apex.

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.