function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
sean.gorman@ipc.comsean.gorman@ipc.com 

Too Many SOQL queries on complex trigger

I know that this has been dealt with many times but I cannot see what more I can do.

 

Account is top level:

    Opportunities are related to Accounts (as expected)

    CEPs are related to Accounts

        Tasks are related to CEPs (for this example)

 

CEPs are a envelope to hold tasks related to the Customer Engagement Plan. A trigger recreates tasks on 2 parameters stored in CEP (different for each account - a way to create recurring tasks without seeing them all created at once)

 

The issue is that we integrate into the system once per hour to load quotes related to opportunities and this updates the opportunity thus causing too many queries.

 

The trigger below finds any CEP tasks owned by the owner of the updated opportunity and completes the task if it is past due or up to 14 days in the future.

 

Anyone got a pointer on how this code could be better?

 

trigger UpdateCEPfromOpp on Opportunity (after insert, after update) {

       /*
        #1 Find the account ID of this opportunity  
        #2 Find all of THIS USERS CEPs related to the account found in #1
        #3 Check to see if there is an End date in the past
        #4 find all of the CURRENT tasks related to the CEPs in #3
        #5 check to see if the tasks are either past due or in next 2 weeks 
        #6 update task, if found, with comment.
        */

	if (!CEPTasksFromOppHelper.hasAlreadyCreatedFollowUpTasks()) 
	{
		for (Opportunity o: trigger.new)
		{
		    date dtWhen;
	    	date dtToday;
	
		    dtToday = system.today();
	    	list<Opportunity> Opps = 
	    	[
		        select id, accountid, ownerid
		        from Opportunity
		        where ID IN :Trigger.newMap.keyset()
		    ];
		    system.debug(Opps);
		    Set<ID> idAccount = New Set<ID>();
		    Set<ID> idOwner = New Set<ID>();

			for (Opportunity Opp : Opps)
	        {
	            idAccount.add(Opp.Accountid);
	            idOwner.add(Opp.OwnerID);
	        }
	        Set<ID> idCEP = New Set<ID>();
	
			if(!Opps.isEmpty())
			{
		        for(Customer_Engagement_Plan__c CEPs : [Select ID, End_Date__c
		                                                from Customer_Engagement_Plan__c CP 
		                                                where (CP.Account__c IN :idAccount and CP.OwnerID in :idOwner)])
		        {
		            if(CEPs.End_Date__c != null)
		            {
		                dtWhen = CEPs.End_Date__c;
		                system.debug(dtWhen);
		                if(dtToday.daysbetween(dtWhen) < 0) 
		                {
		                    idCEP.add(CEPs.ID);
		                }
		            }
		            else
		            {
		                idCEP.add(CEPs.ID);
		            }
		        }
			}
		
		    system.debug(idCEP);
		    list<Task> tasksToUpdate = New list<Task>{};
			if(!idCEP.isEmpty())
			{
				for(Task Tasks : [Select id, accountid, CEP_Activity__c, whatID, Subject, Status, Ownerid, Description, ActivityDate
			                      from Task 
			                      where (WhatID IN :idCEP) and Status <> 'Completed'])
			    {
			    	system.debug(tasks.ActivityDate);
			        system.debug(system.today().daysbetween(tasks.ActivityDate));
			        //if(tasks.ActivityDate.daysbetween(system.today()) < 14)
			        if(system.today().daysbetween(tasks.ActivityDate) < 14)
			        {
			        	date dtDisplay = date.newinstance(o.closedate.year(), o.closedate.month(), o.closedate.day());
			            Tasks.Status = 'Completed';
			            Tasks.Description = 'Opportunity ID ' + o.SFDC_Opportunity_Unique_ID__c + ' was updated. Close date is ' + dtDisplay + ' and Estimated revenue is '+ o.Estimated_Revenue_Amount__c ;
			            tasksToUpdate.add(tasks);
			        }
		     	}
			    if(tasksToUpdate.size() > 0)
			    {
			        update tasksToUpdate;
			    }
			}
			CEPTasksFromOppHelper.setAlreadyCreatedFollowUpTasks();
		}
	}
}

 

Best Answer chosen by Admin (Salesforce Developers) 
sfdcfoxsfdcfox

Here's a much closer version to what you're trying to achieve, but I feel I'm still missing some pieces.

 

trigger UpdateCEPfromOpp on Opportunity (after insert, after update) {
	if (CEPTasksFromOppHelper.hasAlreadyCreatedFollowUpTasks()) {
		return;
	}
	Set<ID> idAccount = New Set<ID>(), idOwner = New Set<ID>();
	Map<Id, map< id, set< Id > > > oppKeys = new map< id, map< id, set< id > > >( );
	Map<Id, Customer_Engagement_Plan__c> plans = new Map< Id, Customer_Engagement_Plan__c>( );
	
	for (Opportunity Opp : Trigger.new) {
		idAccount.add(Opp.Accountid);
		idOwner.add(Opp.OwnerID);
		if( !oppKeys.containsKey( Opp.AccountId ) ) {
			oppKeys.put( Opp.AccountId, new Map< Id, Set< Id > >( ) );
		}
		if( !oppKeys.get( Opp.AccountId ).containsKey( Opp.OwnerId ) ) {
			oppKeys.get( Opp.AccountId ).put( Opp.OwnerId, new Set< Id >( ) );
		}
		oppKeys.get( Opp.AccountId ).get( Opp.OwnerId ).add( Opp.Id );
	}
	plans.putAll([Select Id, Account__c, OwnerId
											from Customer_Engagement_Plan__c
											where Account__c IN :idAccount and OwnerID in :idOwner AND (End_Date__c == null OR End_Date__c > TODAY)]);

	list<Task> tasksToUpdate = New list<Task>{};
	for(Task Tasks : [Select id, accountid, CEP_Activity__c, whatID, Subject, Status, Ownerid, Description, ActivityDate
					  from Task 
					  where (WhatID IN :plans.keyset()) and Status <> 'Completed' AND ActivityDate <= NEXT_N_DAYS:14]) {
		Tasks.Status = 'Completed';
		Tasks.Description = '';
		Customer_Engagement_Plan__c cpe = plans.get( Tasks.WhatId );
		for( Id oppId: oppKeys.get( cpe.Account__c ).get( cpe.OwnerId ) ) {
			Opportunity o = Trigger.newMap.get( oppid );
			Tasks.Description += 'Opportunity ID ' + o.SFDC_Opportunity_Unique_ID__c + ' was updated. Close date is ' + o.closedate.format() + ' and Estimated revenue is '+ o.Estimated_Revenue_Amount__c + '\n';
		}
		tasksToUpdate.add(tasks);
	}
	update tasksToUpdate;
	CEPTasksFromOppHelper.setAlreadyCreatedFollowUpTasks();
}

Here's some problems you had:

 

1) You were querying the entire list of opportunities from the trigger per each opportunity. This is doubly redundant because (a) Trigger.new already has all the information you need, and (b) you wouldn't need to requery the data each time.

 

2) Many useless variables. Sure, it's great to have a variable called dtToday, which is shorter than System.Today(), except that you didn't even use it consistently, so you'd make people wonder if there was some magic going on. Same thing for dtDisplay; the result is you're trying to make a shorter string of code at the expense of more code.

 

3) Filters. You can tell that the code I wrote is more efficient because it asks the database only for tasks that match records it actually intends to process. The code have fewer rows returned, and the code gets to cut out irrelevant if-then statements as a bonus.

 

4) Your code wouldn't work right when using the data loader, for example. This is because the opportunity/task/cpe data would get all jumbled together; you don't actually bother to keep track of the associations, so it would fail to update all the tasks with all the correct opportunity information. In my example code, we we "maps" to keep our data organized. Note that my code precludes the possibility that a description field might notate that multiple opportunities were the catalyst for the task update. This isn't strictly necessary, but it might be useful to know.

 

5) I have a feeling that SFDC_Opportunity_Unique_ID__c is actually a formula for "Id". If so, you probably should just use Id instead. If it's an auto-number, that might not be the best idea, for pratical reasons (someone could reset the auto-number index and confuse people).

 

6) Ditto for Estimated_Revenue_Amount__c. Salesforce already has a field that does something like this, so this might be redundant. But, I know many organizations don't like the default definition of "estimated revenue" (probably because they don't understand its purpose), so if that field makes sense, then feel free to use it. Just an observation.

 

7) I'm not sure you really, really meant to query based on OwnerId/AccountId, but I built this code with that assumption. This sort of query sets of warning bells in my head that something might be amiss. Of course, I could be wrong, and if it helps you out, great.

 

8) Things might still be broke in my example above, but I hope it helps you get where you're trying to go.

All Answers

Suresh RaghuramSuresh Raghuram

first thing i would like to ask you is why you are writing queries in for loop.

 

Any opportunity will have a loopkup to the contact or account.

 

to get all the opp ids  simply 

set<Id> opportunityIds set<Id>();

set<Id> accountIds set<Id>();

for (opportunity opp; trigger.new){

opportunityIds.add(opp.Id);

opportunityIds.add(opp.AccountId);

}

Make your sets public i mean define them out of for loop and use them where wver necessary.

 

 

sean.gorman@ipc.comsean.gorman@ipc.com

Thank you for your response. Good idea to take that loop out but it leaves me with another problem: I need to post a lucid comment in the task as it completes:

 

	        	date dtDisplay = date.newinstance(o.closedate.year(), o.closedate.month(), o.closedate.day());
	            Tasks.Status = 'Completed';
	            Tasks.Description = 'Opportunity ID ' + o.SFDC_Opportunity_Unique_ID__c + ' was updated. Close date is ' + dtDisplay + ' and Estimated revenue is '+ o.Estimated_Revenue_Amount__c ;
	            tasksToUpdate.add(tasks);

 The collection in list: o guarantees that I have the correct ID and Estimated revenue amounts.

 

By taking out that loop, how can I be sure to post the correct information?

sfdcfoxsfdcfox

Here's a much closer version to what you're trying to achieve, but I feel I'm still missing some pieces.

 

trigger UpdateCEPfromOpp on Opportunity (after insert, after update) {
	if (CEPTasksFromOppHelper.hasAlreadyCreatedFollowUpTasks()) {
		return;
	}
	Set<ID> idAccount = New Set<ID>(), idOwner = New Set<ID>();
	Map<Id, map< id, set< Id > > > oppKeys = new map< id, map< id, set< id > > >( );
	Map<Id, Customer_Engagement_Plan__c> plans = new Map< Id, Customer_Engagement_Plan__c>( );
	
	for (Opportunity Opp : Trigger.new) {
		idAccount.add(Opp.Accountid);
		idOwner.add(Opp.OwnerID);
		if( !oppKeys.containsKey( Opp.AccountId ) ) {
			oppKeys.put( Opp.AccountId, new Map< Id, Set< Id > >( ) );
		}
		if( !oppKeys.get( Opp.AccountId ).containsKey( Opp.OwnerId ) ) {
			oppKeys.get( Opp.AccountId ).put( Opp.OwnerId, new Set< Id >( ) );
		}
		oppKeys.get( Opp.AccountId ).get( Opp.OwnerId ).add( Opp.Id );
	}
	plans.putAll([Select Id, Account__c, OwnerId
											from Customer_Engagement_Plan__c
											where Account__c IN :idAccount and OwnerID in :idOwner AND (End_Date__c == null OR End_Date__c > TODAY)]);

	list<Task> tasksToUpdate = New list<Task>{};
	for(Task Tasks : [Select id, accountid, CEP_Activity__c, whatID, Subject, Status, Ownerid, Description, ActivityDate
					  from Task 
					  where (WhatID IN :plans.keyset()) and Status <> 'Completed' AND ActivityDate <= NEXT_N_DAYS:14]) {
		Tasks.Status = 'Completed';
		Tasks.Description = '';
		Customer_Engagement_Plan__c cpe = plans.get( Tasks.WhatId );
		for( Id oppId: oppKeys.get( cpe.Account__c ).get( cpe.OwnerId ) ) {
			Opportunity o = Trigger.newMap.get( oppid );
			Tasks.Description += 'Opportunity ID ' + o.SFDC_Opportunity_Unique_ID__c + ' was updated. Close date is ' + o.closedate.format() + ' and Estimated revenue is '+ o.Estimated_Revenue_Amount__c + '\n';
		}
		tasksToUpdate.add(tasks);
	}
	update tasksToUpdate;
	CEPTasksFromOppHelper.setAlreadyCreatedFollowUpTasks();
}

Here's some problems you had:

 

1) You were querying the entire list of opportunities from the trigger per each opportunity. This is doubly redundant because (a) Trigger.new already has all the information you need, and (b) you wouldn't need to requery the data each time.

 

2) Many useless variables. Sure, it's great to have a variable called dtToday, which is shorter than System.Today(), except that you didn't even use it consistently, so you'd make people wonder if there was some magic going on. Same thing for dtDisplay; the result is you're trying to make a shorter string of code at the expense of more code.

 

3) Filters. You can tell that the code I wrote is more efficient because it asks the database only for tasks that match records it actually intends to process. The code have fewer rows returned, and the code gets to cut out irrelevant if-then statements as a bonus.

 

4) Your code wouldn't work right when using the data loader, for example. This is because the opportunity/task/cpe data would get all jumbled together; you don't actually bother to keep track of the associations, so it would fail to update all the tasks with all the correct opportunity information. In my example code, we we "maps" to keep our data organized. Note that my code precludes the possibility that a description field might notate that multiple opportunities were the catalyst for the task update. This isn't strictly necessary, but it might be useful to know.

 

5) I have a feeling that SFDC_Opportunity_Unique_ID__c is actually a formula for "Id". If so, you probably should just use Id instead. If it's an auto-number, that might not be the best idea, for pratical reasons (someone could reset the auto-number index and confuse people).

 

6) Ditto for Estimated_Revenue_Amount__c. Salesforce already has a field that does something like this, so this might be redundant. But, I know many organizations don't like the default definition of "estimated revenue" (probably because they don't understand its purpose), so if that field makes sense, then feel free to use it. Just an observation.

 

7) I'm not sure you really, really meant to query based on OwnerId/AccountId, but I built this code with that assumption. This sort of query sets of warning bells in my head that something might be amiss. Of course, I could be wrong, and if it helps you out, great.

 

8) Things might still be broke in my example above, but I hope it helps you get where you're trying to go.

This was selected as the best answer
sean.gorman@ipc.comsean.gorman@ipc.com

Wow - thank you fox. I am putting this into test to check the veracity and then see if it works.

 

Replies:

#1 Thank you - yes I saw that after the previous commenter and then I fixed it but it left me with more problems (see #4).

 

#2 Yes. The dispay of close date includes time, which I didn't want. I am assuming that o.closedate.format() does the same thing?

 

#3 I like the way that you've done that - I did't know that you could.

 

#4 Yes. I didn't know how to fix this when removing the top loop.

 

#5 It is our controlling field for ID of opportunities.

 

#6 Totally a different field from SFDC's own version. SFDC PS created this.

 

#7 I see where you have concerns here but there is no account ownership by sales people in sfdc, but they do generally run opportunities on the same sets of accounts. They also have an engagement plan for some of their accounts. This trigger is to help them by completing tasks that they have to close on set intervals on their accounts when an opportunity on one of those accounts is updated. They do not have a CEP on every one of their accounts and may have opportunities on accounts where someone else has a CEP.

The position is that where (o.owner and o.account) = (cep.owner and cep.account) then I will update their CEP Task on their behalf.

 

#8 Thank you so much for your help. I think this is amazing.

 

 

sean.gorman@ipc.comsean.gorman@ipc.com

After testing in the sandbox, I pushed the triger to live only to find that I had made an assumption that they would not be deleting the tasks related to their CEPs.

 

Because of that assumption I was getting an:

 

Attempt to de-reference a null object

 

They are not now deleting tasks (as behaviour has been modified), and for those that they did in the past I have written an EndDate into the CEP to prevent the error now - but I am trying to add to the trigger to prevent errors occurring in the future.

 

I tried to add a catch to check that the Tasks were not empty [ If ( !tasks.isempty() ) ]. But that does not work for objects.

 

trigger UpdateCEPfromOpp on Opportunity (after insert, after update) {
	if (CEPTasksFromOppHelper.hasAlreadyCreatedFollowUpTasks()) {
		return;
	}
	Set<ID> idAccount = New Set<ID>(), idOwner = New Set<ID>();
	Map<Id, map< id, set< Id > > > oppKeys = new map< id, map< id, set< id > > >( );
	Map<Id, Customer_Engagement_Plan__c> plans = new Map< Id, Customer_Engagement_Plan__c>( );
	
	for (Opportunity Opp : Trigger.new) {
		idAccount.add(Opp.Accountid);
		idOwner.add(Opp.OwnerID);
		if( !oppKeys.containsKey( Opp.AccountId ) ) {
			oppKeys.put( Opp.AccountId, new Map< Id, Set< Id > >( ) );
		}
		if( !oppKeys.get( Opp.AccountId ).containsKey( Opp.OwnerId ) ) {
			oppKeys.get( Opp.AccountId ).put( Opp.OwnerId, new Set< Id >( ) );
		}
		oppKeys.get( Opp.AccountId ).get( Opp.OwnerId ).add( Opp.Id );
	}
	plans.putAll([Select Id, Account__c, OwnerId
											from Customer_Engagement_Plan__c
											where Account__c IN :idAccount and OwnerID in :idOwner AND (End_Date__c = null OR End_Date__c > TODAY)]);

	system.debug('Plans = ' +plans);
	list<Task> tasksToUpdate = New list<Task>{};
	for(Task Tasks : [Select id, accountid, CEP_Activity__c, whatID, Subject, Status, Ownerid, Description, ActivityDate
					  from Task 
					  where (WhatID IN :plans.keyset()) and Status <> 'Completed' AND ActivityDate <= NEXT_N_DAYS:14]) {
//		if(!tasks.isempty()) {
			Tasks.Status = 'Completed';
			Tasks.Description = '';
			Customer_Engagement_Plan__c cpe = plans.get( Tasks.WhatId );
			system.debug('CEPs = ' +cpe);
			for( Id oppId: oppKeys.get( cpe.Account__c ).get( cpe.OwnerId ) ) {
				Opportunity o = Trigger.newMap.get( oppid );
				Tasks.Description += 'Opportunity ID ' + o.SFDC_Opportunity_Unique_ID__c + ' was updated. Close date is ' + o.closedate.format() + ' and Estimated revenue is '+ o.Estimated_Revenue_Amount__c + '\n';
			}
			tasksToUpdate.add(tasks);
	     //}
} system.debug('tasksToUpdate = ' +tasksToUpdate); update tasksToUpdate; CEPTasksFromOppHelper.setAlreadyCreatedFollowUpTasks(); }

 Any Ideas?

sfdcfoxsfdcfox

2) Nope, Date.format() returns a date, without the time, formatted in the user's current locale. For users configured in the USA, that means that it will appear as "m/d/yyyy" (2/11/2013, for example).

 

3) Given salesforce.com's tight governor limits, it's important to try and filter as selectively as possible.

 

Now, in regards to the "null pointer" bit, you'll need a line number. You'll have to figure out which line it is, and then diagnose it from there.

sean.gorman@ipc.comsean.gorman@ipc.com

Hi Fox,

 

Thank you for your help on this..

 

The error is in the line

        for( Id oppId: oppKeys.get( cpe.Account__c ).get( cpe.OwnerId ) )

 

I did a dubug on this oppKeys and got this (formatting my own):

 
08:09:32.668 (2668157000)|USER_DEBUG|[35]|DEBUG|oppKeys = {
0018000000={005C000000={006C000000}},
0018000000={0058000000={006C000000}},
0018000000={0058000000={006C000000, 006C000000, 006C000000, 006C000000}, 0058000000={006C000000}},
0018000000={0058000000={006C000000}},
0018000000={0058000000={006C000000, 006C000000}},
0018000000={0058000000={006C000000, 006C000000}},
0018000000={0058000000={006C000000}},
0018000000={0058000000={006C000000}, 0058000000={006C000000, 006C000000}},
0018000000={005C000000={006C000000}},
0018000000={0058000000={006C000000, 006C000000}, 0058000000={006C000000}, 0058000000={006C000000, 006C000000, 006C000000, 006C000000}}, ...}

08:09:32.668 (2668833000)|FATAL_ERROR|System.NullPointerException: Attempt to de-reference a null object

 

It looks a bit strange but I am not sure if there are meant to be different numbers of entries in the oppKeys for each.

sfdcfoxsfdcfox

The last five characters of each ID is missing... Intentional? Well, anyways, maybe you could change this part:

 

Tasks.Status = 'Completed';
			Tasks.Description = '';
			Customer_Engagement_Plan__c cpe = plans.get( Tasks.WhatId );
			system.debug('CEPs = ' +cpe);
			for( Id oppId: oppKeys.get( cpe.Account__c ).get( cpe.OwnerId ) ) {
				Opportunity o = Trigger.newMap.get( oppid );
				Tasks.Description += 'Opportunity ID ' + o.SFDC_Opportunity_Unique_ID__c + ' was updated. Close date is ' + o.closedate.format() + ' and Estimated revenue is '+ o.Estimated_Revenue_Amount__c + '\n';
			}
			tasksToUpdate.add(tasks);

 as follows:

 

    Tasks.Status = 'Completed';
    Tasks.Description = '';
    Customer_Engagement_Plan__c cpe = plans.get( Tasks.WhatId );
    system.debug('CEPs = ' +cpe);
    if( oppKeys.containsKey( cpe.Account ) && oppKeys.get( cpe.OwnerId ) ) {
        for( Id oppId: oppKeys.get( cpe.Account__c ).get( cpe.OwnerId ) ) {
            Opportunity o = Trigger.newMap.get( oppid );
            Tasks.Description += 'Opportunity ID ' + o.SFDC_Opportunity_Unique_ID__c + ' was updated. Close date is ' + o.closedate.format() + ' and Estimated revenue is '+ o.Estimated_Revenue_Amount__c + '\n';
        }
    }
    tasksToUpdate.add(tasks);

Other than that, I'm not sure how to help, because I simply don't have the time to set up a complete mockup of your organization's metadata at the moment. All I know is that this code should be working. What line of code is the error cropping up at?