Français
Presentations About Resources

Salesforce, Python, SQL, & other ways to put your data where you need it -- a bilingual blog in English & French

Salesforce Apex Performance: new ID map vs. for loop

08 Jul 2019 🔖 salesforce tips triggers
💬 EN

Table of Contents

Salesforce developers: Have you ever hand-built a Map of SObjects by ID with a for loop? Are you sure you should?

Don’t forget about the magic of Apex’s keyword “new.”

It runs faster and can help reduce your CPU limit consumption.

The problem

We just started leveraging Salesforce’s EDA / HEDA package’s TDTM (table-driven trigger management) framework for controlling the flow of triggers we add to our org.

One thing that’s missing from the signature of the hed__TDTM_Runnable.run() method that every TDTM-compatible trigger handler must extend is a reference to the contents of Trigger.oldMap.

Trigger.old is passed through .run(), but not Trigger.oldMap.

This makes comparing members of Trigger.new to their “old” counterparts a bit of a pain, so of course one of the first things you may need to do by hand within an implementation of .run() that will be handling update triggers is give yourself a Map<Id, YourSObject> oldMap to work with.

The slow way

You might think to do it like this:

Map<Id, Contact> oldMap;
if (oldList != null) {
	oldMap = new Map<Id, Contact>();
	for (Contact c : (List<Contact>)oldList) {
		oldMap.put(c.Id, c);
	}
}

(Note: in this case, I’m casting oldList to a List<Contact> because it comes from TDTM’s run() parameters as a List<SObject>, being a reference to Trigger.new.)

However, it turns out that manually looping over a List to populate a Map isn’t always the best way to use your precious CPU miliseconds.

The fast way

If you are sure that your every member of your List<YourSObject> will have a non-null Id attribute, then you can use a shortcut Apex command that runs faster than a manual loop:

Map<Id, Contact> oldMap;
if (oldList != null) {
	oldMap = new Map<Id,Contact>((List<Contact>)oldList);
}

Or, if you’re saucy and love ternary operators:

Map<Id, Contact> oldMap = (oldList != null) ? new Map<Id,Contact>((List<Contact>)oldList) : NULL ;

(Reminder: you only need to cast yourInputList to a (List<YourSObject>) if you have reason to believe that yourInputList contains a plain old List<SObject>.)

Performance comparison

I made a little unit test, MiscUnitTest to be sure I wasn’t imagining things:

@isTest
public class MiscUnitTest {
    static testMethod void runTest() {
        List<Contact> cs = new List<Contact>();
        for (Integer i = 0; i < 200; i++) { cs.add(new Contact(LastName= 'c'+i)); }
        INSERT cs;
        Test.startTest();
        UPDATE cs;
        Test.stopTest();
    }
    
}

To make it actually put anything interesting in the debug logs, I added this block of code to an existing Contact trigger handler:

if (isUpdate) {
	System.debug(LoggingLevel.ERROR, 'PERFTEST-abcStart' + ', CPU:' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
	for (Integer i = 0; i < 1000; i++) {
		Map<Id, Contact> abc = new Map<Id, Contact>();
		for (Contact c : (List<Contact>)Trigger.old) {
			abc.put(c.Id, c);
		}
	}
	System.debug(LoggingLevel.ERROR, 'PERFTEST-abcEnd' + ', CPU:' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
}

if (isUpdate) {
	System.debug(LoggingLevel.ERROR, 'PERFTEST-xyzStart' + ', CPU:' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
	for (Integer i = 0; i < 1000; i++) {
		Map<Id, Contact> xyz = new Map<Id, Contact>((List<Contact>)Trigger.old);
	}
	System.debug(LoggingLevel.ERROR, 'PERFTEST-xyzEnd' + ', CPU:' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
}

The org I was testing in has enough complications going on with automation for Contact records that this code executed twice when I ran my test.

  • In the first pass, performing the manual for loop 1,000 times in a row took 3,236 miliseconds. In the second pass, it took 3,618 miliseconds.
  • In the first pass, performing the new Map<Id...>() approach 1,000 times in a row took 364 miliseconds. In the second pass, it took 443 miliseconds.

Note that I didn’t always see a 90% improvement. With a unit test involving fewer records, the CPU savings could be more like 33%.

Nevertheless, the “native shortcut” always beat a manual for loop in my tests.

Takeaway

Moral of the story: if you need a Map of SObject records from a List of them and want to use Id as the key, don’t build the loop by hand.

Instead, pass your List to new Map<Id, YourSObject> the same way you would auto-populate Map entries from a SOQL query.

Warning: be sure that every member of the List has a non-null Id or you’ll be in for an unpleasant surprise at runtime!

--- ---