An Apex Set is an unordered collection of unique elements - it never stores duplicates, so it is the go-to collection for membership tests and de-duplication in Salesforce. Because a Set rejects repeats automatically, the most common real-world use is collecting record Ids into a Set<Id> and binding it straight into a SOQL WHERE Id IN :idSet clause, which keeps queries out of loops and inside governor limits. The methods you reach for daily are add(), addAll(), contains(), containsAll(), remove(), removeAll(), retainAll(), size(), isEmpty(), and clear().
This guide covers how to declare a Set, every method that matters with short examples, the Set<Id> SOQL bind pattern, using a Set to prevent trigger recursion, and how Set fits alongside List and Map in the Apex collections trio.
Key takeaways
- A
Set<T>is unordered and holds only unique values - adding a duplicate is silently ignored, so it is the simplest way to de-duplicate. - There is no index access: you cannot call
get(0)on a Set. You test membership withcontains()or iterate with aforloop. - Core methods:
add,addAll,remove,removeAll,retainAll,contains,containsAll,clear,isEmpty,size,clone,equals. - The #1 use is collecting a
Set<Id>and binding it into SOQL asWHERE Id IN :idSet- one query for many records, instead of querying inside a loop. - A
Setis the cleanest way to track processed record Ids and prevent trigger recursion. - Element ordering is not guaranteed; for
Stringvalues, Set membership is case-sensitive. - Use a
Setfor uniqueness, aListfor ordered/indexed data, and aMapfor key-value lookups.
What is an Apex Set in Salesforce?
A Set is a typed collection with two defining properties:
- Unique - it cannot contain duplicate values. Adding a value that already exists is a no-op.
- Unordered - elements are not stored by position, and iteration order is not guaranteed. You therefore cannot read an element by index the way you do with a
List.
Elements can be any data type: primitives (String, Integer, Id, Decimal), sObjects, or user-defined classes. You declare a Set with new Set<DataType>() - empty, or seeded inline with curly braces. Because Sets discard duplicates for free, they are the natural fit any time you need "the distinct set of X" - distinct Account Ids, distinct picklist values, distinct emails.
// Declare an empty Set, then add later
Set<String> names = new Set<String>();
// Initialize with values inline
Set<String> fruits = new Set<String>{ 'apple', 'banana', 'cherry' };
// Duplicates are silently ignored
Set<String> tags = new Set<String>{ 'vip', 'vip', 'new' };
System.debug(tags.size()); // 2 -> only 'vip' and 'new'
// A Set of record Ids - the most common Salesforce use
Set<Id> accountIds = new Set<Id>();How do you add, check, and remove elements?
add(element)- adds an element; returnstrueif it was new,falseif it was already present.addAll(listOrSet)- adds every element from aListor anotherSetof the same type.contains(element)-trueif the value is present (this is what a Set is built for - fast membership tests).containsAll(listOrSet)-trueif this Set contains every element of the supplied collection.remove(element)- removes the element; returnstrueif it was present.removeAll(listOrSet)- removes every element found in the supplied collection (set difference).retainAll(listOrSet)- keeps only the elements also found in the supplied collection (set intersection).
Set<String> names = new Set<String>();
Boolean added = names.add('John'); // true -> ['John']
names.add('Kelly'); // true -> ['John', 'Kelly']
Boolean again = names.add('John'); // false -> already present, still 2 elements
names.addAll(new List<String>{ 'Paul', 'Sara' }); // add all from a List
Boolean hasKelly = names.contains('Kelly'); // true
Boolean hasAll = names.containsAll(new List<String>{ 'John', 'Paul' }); // true
names.remove('Paul'); // removes 'Paul'
names.removeAll(new List<String>{ 'Sara' }); // set difference
names.retainAll(new List<String>{ 'John' }); // intersection -> keeps only 'John'How do you check size, copy, and compare a Set?
size()- the number of elements (Integer).isEmpty()-truewhensize()is 0.clear()- removes every element.clone()- returns a shallow copy of the Set.equals(set)-trueif both Sets contain exactly the same elements (order is irrelevant);==does the same.
Set<Integer> nums = new Set<Integer>{ 10, 20, 30 };
Integer count = nums.size(); // 3
Boolean empty = nums.isEmpty(); // false
Set<Integer> copy = nums.clone(); // shallow copy
// equals()/== compare contents, not order (a Set is unordered)
Set<Integer> a = new Set<Integer>{ 1, 2, 3 };
Set<Integer> b = new Set<Integer>{ 3, 2, 1 };
Boolean same = a.equals(b); // true (a == b is also true)
nums.clear(); // {} -> size() == 0Apex Set method reference
| Method | Signature | What it does |
|---|---|---|
| add | Boolean add(Object e) |
Add an element; returns true if it was new (duplicates ignored) |
| addAll | Boolean addAll(List l) / Boolean addAll(Set s) |
Add all elements from a List or Set |
| remove | Boolean remove(Object e) |
Remove an element; returns true if it was present |
| removeAll | Boolean removeAll(List l) / Boolean removeAll(Set s) |
Remove all elements found in the collection (difference) |
| retainAll | Boolean retainAll(List l) / Boolean retainAll(Set s) |
Keep only elements also in the collection (intersection) |
| contains | Boolean contains(Object e) |
true if the value is present |
| containsAll | Boolean containsAll(List l) / Boolean containsAll(Set s) |
true if every supplied element is present |
| clear | void clear() |
Remove all elements |
| isEmpty | Boolean isEmpty() |
true when size is 0 |
| size | Integer size() |
Number of elements |
| clone | Set clone() |
Shallow copy of the Set |
| equals | Boolean equals(Set s) |
true when both Sets hold the same elements |
Note: A Set is unordered - there is no get(index), sort(), or indexOf(). If you need ordering or index access, use a List instead.
How do you use a Set as a SOQL IN bind variable?
This is the single most common reason to use a Set in Salesforce. You loop once over your records, collect the related Ids into a Set<Id> (duplicates vanish automatically), then bind that Set into a single SOQL query with WHERE Id IN :idSet. This pulls the query out of the loop and keeps you inside governor limits - one query for the whole batch instead of one per record. For the query syntax itself, see how to run a SOQL query in Salesforce.
// Triggers and Data Loader process records in bulk, so never query in a loop.
List<Contact> contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId != null];
// 1. Collect unique parent Ids - a Set drops duplicate AccountIds for free
Set<Id> accountIds = new Set<Id>();
for (Contact c : contacts) {
accountIds.add(c.AccountId);
}
// 2. ONE bulk-safe query, binding the Set into the IN clause
List<Account> parents = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
// The same Set also works as the key source for a Map-based lookup
Map<Id, Account> accountById = new Map<Id, Account>(parents);
for (Contact c : contacts) {
Account a = accountById.get(c.AccountId); // O(1), no SOQL in the loop
System.debug(c.Id + ' -> ' + a.Name);
}How do you use a Set to prevent trigger recursion?
When a trigger updates records, those updates can fire the same trigger again, causing infinite recursion or hitting governor limits. A classic guard is a static Set<Id> that records which Ids have already been processed in the current transaction; you skip any Id the Set already contains. Because a Set's contains()/add() are built for exactly this membership check, it is the natural tool for the job.
public class AccountTriggerHandler {
// static, so it persists across re-entrant trigger invocations in one transaction
private static Set<Id> processedIds = new Set<Id>();
public static void handleAfterUpdate(List<Account> records) {
List<Account> toProcess = new List<Account>();
for (Account a : records) {
if (!processedIds.contains(a.Id)) { // skip Ids already handled
processedIds.add(a.Id);
toProcess.add(a);
}
}
// ... safe to run logic/DML on toProcess without re-triggering itself
}
}De-duplicating values with a Set
Beyond Ids, Sets are the cleanest way to collapse any collection down to its distinct values. Pass a List into a Set constructor and the duplicates disappear in one line - useful for distinct emails, picklist values, or external keys, and for catching duplicate rows before an insert or upsert.
// De-duplicate a List in one line by constructing a Set from it
List<String> rawEmails = new List<String>{ 'a@x.com', 'b@x.com', 'a@x.com' };
Set<String> uniqueEmails = new Set<String>(rawEmails); // {'a@x.com', 'b@x.com'}
// Detect duplicates while building a collection
Set<String> seen = new Set<String>();
List<String> duplicates = new List<String>();
for (String email : rawEmails) {
if (!seen.add(email)) { // add() returns false if already present
duplicates.add(email); // 'a@x.com' captured as a duplicate
}
}Set vs List vs Map: which Apex collection should you use?
Set, List, and Map are the three Apex collection types, and they solve different problems:
| Collection | Ordered? | Duplicates? | Accessed by | Best for |
|---|---|---|---|---|
| Set | No | No (unique values) | Membership (contains) |
Collecting unique values, de-duplicating Ids, SOQL IN binds |
| List | Yes (insertion order) | Yes | Index (get(i)) |
Ordered data, SOQL results, batching records for DML |
| Map | No (key order not guaranteed) | Unique keys | Key (get(key)) |
Key-to-value lookups, grouping records by a field such as Id |
A common real-world flow uses all three: query a List from SOQL, pull unique parent Ids into a Set, then build a Map keyed by Id for O(1) lookups inside a loop. For the other two collections, see our companion guides on Apex List methods in Salesforce for ordered, indexed data, and Apex Map methods in Salesforce for key-value lookups.
Frequently Asked Questions
What is a Set in Salesforce Apex?
A Set is an Apex collection that holds an unordered group of unique elements. It never stores duplicates, so adding a value that already exists has no effect. Sets are optimised for membership tests with contains() and are the standard tool for de-duplication and for collecting distinct record Ids.
What is the difference between a Set and a List in Apex?
A List is ordered, allows duplicates, and is accessed by a zero-based index. A Set is unordered, stores only unique values, and has no index access - you test membership with contains() instead of get(i). Use a List when order or duplicates matter (such as SOQL results), and a Set when you need uniqueness, like collecting distinct Account Ids.
How do I remove duplicates from a List in Apex?
Pass the List straight into a Set constructor: Set<String> unique = new Set<String>(myList);. The Set discards duplicates automatically. If you then need an ordered, indexed collection back, construct a new List from the Set with new List<String>(unique).
Can I access a Set element by index?
No. A Set is unordered, so there is no get(index), sort(), or indexOf() method, and iteration order is not guaranteed. To read elements, either iterate with a for loop or convert the Set to a List first if you need positional access.
How do I use a Set in a SOQL query?
Collect your values into a Set<Id> (or Set<String>) and bind it into the query with the IN operator: [SELECT Id FROM Account WHERE Id IN :accountIds]. Building the Set in a loop and then running one bound query is the standard bulkification pattern that keeps SOQL out of loops and inside governor limits.
Are Apex Set elements case-sensitive for Strings?
Yes. For a Set<String>, 'VIP' and 'vip' are treated as two distinct elements, so both can coexist in the same Set. If you want case-insensitive uniqueness, normalise the case (for example with toLowerCase()) before adding values to the Set.
Build robust Apex on Salesforce
Mastering Sets - and the Set<Id> SOQL bind pattern they enable - is what turns a query-in-a-loop trigger into bulk-safe code that survives a 50,000-record data load. At MicroPyramid we have delivered Salesforce solutions for 12+ years across 50+ projects, writing governor-limit-safe triggers, services, and integrations that scale.
Need a hand with Apex, triggers, or a Salesforce build? Explore our Salesforce development services and let's talk about your project.