Salesforce Apex Performance: new ID map vs. for loop
08 Jul 2019
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!