Press ESC to close

Working with Aggregate SOQL queries/results in Batch Apex !

What if you want create Batch Apex Job, that uses SOQL having aggregate functions like SUM, MAX, COUNT on top of results grouped by “GROUP BY” clause. You can’t easily create a Batch job via QueryLocator, as shown below.

global class QueryLocatorAggregator implements Database.Batchable<AggregateResult> {
}

It fails for this error

Error: Compile Error: QueryLocatorAggregator: Class must implement the global interface method: Iterable<SOBJECT:AggregateResult> start(Database.BatchableContext) from Database.Batchable<SOBJECT:AggregateResult> at line 1 column 14

So the other option that seems doable is using SObject as generic type argument in Batchable, as shown below with complete source code.

global class QueryLocatorAggregator implements Database.Batchable<Sobject> {
    global Database.QueryLocator start(Database.BatchableContext info){
        String query = 'Select Department, COUNT(Name) From Contact GROUP BY Department';
        return Database.getQueryLocator(query);
    }
    global void execute(Database.BatchableContext BC,
             List<Sobject> scope){
        for (Sobject so : scope)  {
          AggregateResult ar = (AggregateResult) so;
          Integer counter = Integer.valueOf(ar.get('expr0'));
          // process the results
        }
    }
    global void finish(Database.BatchableContext BC){}   
}

The above code/class complies(saves), but on first processing of batch it fails with this error

System.UnexpectedException: Aggregate query does not support queryMore(), use LIMIT to restrict the results to a single batch

So, how to work on queries with aggregation ? well the hint was right their in the first error message in red i.e. use Iterable<AggregateResult>

Making AggregateResult queries work in Batch Apex

The only way I found is using Iterable<??>. Iterable is an easy QueryLocator alternate to feed data to batch apex execution. Database.Batchable<?> interface gives two abstract methods to implement

  1. global (Database.QueryLocator ) start(Database.BatchableContext bc) {}

  2. global (Iterable<sObject>) start(Database.BatchableContext bc) {}

The magical part about Database.Batchable interface is you need to implement either of the above two abstract methods. Though those magic confuse me a lot, because of my Java background 😀

Here are the three steps to get rolling with Iterable<> in Batch Apex

  1. Create a class that implements contract of Iterator<AggregateResult>

  2. Create a class that implements contract of Iterable<AggregateResult>

  3. Use the instance of Iterable<AggregateResult> implementation in Database.Batchable’s “global (Iterable<sObject>) start(Database.BatchableContext bc) {}”

Create a class that implements contract of Iterator<AggregateResult>

Here is the code sample for this.

global class AggregateResultIterator implements Iterator<AggregateResult> {
   AggregateResult [] results {get;set;}
   // tracks which result item is returned
   Integer index {get; set;} 
         
   global AggregateResultIterator() {
      index = 0;
	  // Fire query here to load the results
      String query = 'Select Department, COUNT(Name) From Contact GROUP BY Department';
      results = Database.query(query);            
   } 
   
   global boolean hasNext(){ 
      return results != null && !results.isEmpty() && index < results.size(); 
   }    
   
   global AggregateResult next(){        
      return results[index++];            
   }       
}    
Create a class that implements contract of Iterable<AggregateResult>

Here is the code sample for that

global class AggregateResultIterable implements Iterable<AggregateResult> {
   global Iterator<AggregateResult> Iterator(){
      return new AggregateResultIterator();
   }
}
Use the instance of Iterable<AggregateResult> implementation in Database.Batchable’s “global (Iterable<sObject>) start(Database.BatchableContext bc) {}”

Here is the complete Database.Batchable class implementation

global class IterableAggregator implements Database.Batchable<AggregateResult> {    
    global Iterable<AggregateResult> start(Database.batchableContext info){
        // just instantiate the new iterable here and return
        return new AggregateResultIterable();
    }

    global void execute(Database.BatchableContext BC, List<Sobject> scope){
        for (Sobject so : scope)  {
          AggregateResult ar = (AggregateResult) so;
          Integer counter = Integer.valueOf(ar.get('expr0'));
          // process the results
        }        
    }

    global void finish(Database.BatchableContext BC){}   
}

When not to use Iterable<?> with AggregateResult

Database.Batchable implementations depending on Iterable<?> for data source are bound to normal governor limits, so Iterable’s can never process 50 million records like QueryLocator. Before using Iterable, you need to watch the amount of Data/Rows resulting from that Aggregated SOQL.

References

Comments (14)

  • Anonymoussays:

    July 12, 2011 at 3:53 am

    Yes, with aggregation batch works like normal apex, you can't expect normal limits. The same is mentioned in the post i.e.”When not to use Iterable with AggregateResultDatabase.Batchable implementations depending on Iterable for data source are bound to normal governor limits, so Iterable’s can never process 50 million records like QueryLocator. Before using Iterable, you need to watch the amount of Data/Rows resulting from that Aggregated SOQL.”

  • Anonymoussays:

    September 9, 2011 at 11:57 am

    I have a simple class:public class CountCase { public static void countCasesInAccount(){ List results = [select AccountId, count(ID) total from Case group by AccountId ]; for (AggregateResult ar : results) System.debug('============== '+ar.get('AccountId')+'-'+ar.get('total')); } static testMethod void testBatchClass() { CountCase.countCasesInAccount(); }}This is not returning first AccountId, below is result:04:41:17.498 (498742000)|USER_DEBUG|[6]|DEBUG|============== null-704:41:17.499 (499019000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBfAAK-204:41:17.499 (499303000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBgAAK-104:41:17.499 (499562000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBhAAK-304:41:17.499 (499830000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBiAAK-304:41:17.500 (500088000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBjAAK-204:41:17.500 (500359000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBmAAK-404:41:17.500 (500618000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBnAAK-304:41:17.500 (500913000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBoAAK-104:41:17.501 (501186000)|USER_DEBUG|[6]|DEBUG|============== 00190000006KcBpAAK-704:41:17.501 (501468000)|USER_DEBUG|[6]|DEBUG|============== 00190000006XfvnAAC-104:41:17.501 (501744000)|USER_DEBUG|[6]|DEBUG|============== 00190000007PKwEAAW-27May I know how to resolve it.

  • Anonymoussays:

    October 3, 2011 at 6:41 pm

    I was really stumped trying to get some batch apex to run on an aggregate query on a custom object. I kept getting “System.UnexpectedException: Field must be grouped or aggregated”this solved it. thanks a ton for this post.-CWD

  • Anonymoussays:

    June 13, 2012 at 5:51 pm

    I am running into a Heap Size Problem when trying to aggregate 50.000 records using you method:18:34:04.902 (40902037000)|METHOD_ENTRY|[410]|01pd0000001mTzH|VolumeDataAggregator.GroupByResultIterator.__sfdc_results()*********** MAXIMUM DEBUG LOG SIZE REACHED ***********18:34:14.543 (50543116000)|FATAL_ERROR|System.LimitException: Apex heap size too large: 44262261Any idea how to circumvent this?Robert

  • Anonymoussays:

    June 14, 2012 at 3:08 am

    Seems the queried fields are having huge data in them, for ex. a text area field with 32000 chars. If this is the case, I would suggest not to query that field in this step ”    global Database.QueryLocator start(Database.BatchableContext info)” but try reloading the record with required fields in ”    global void execute(Database.BatchableContext BC..”HTH !

Leave a Reply

%d bloggers like this: