Apex Set Methods in Salesforce: A Complete Guide

Blog / Salesforce · December 25, 2016 · Updated June 10, 2026 · 9 min read
Apex Set Methods in Salesforce: A Complete Guide

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 with contains() or iterate with a for loop.
  • 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 as WHERE Id IN :idSet - one query for many records, instead of querying inside a loop.
  • A Set is the cleanest way to track processed record Ids and prevent trigger recursion.
  • Element ordering is not guaranteed; for String values, Set membership is case-sensitive.
  • Use a Set for uniqueness, a List for ordered/indexed data, and a Map for 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; returns true if it was new, false if it was already present.
  • addAll(listOrSet) - adds every element from a List or another Set of the same type.
  • contains(element) - true if the value is present (this is what a Set is built for - fast membership tests).
  • containsAll(listOrSet) - true if this Set contains every element of the supplied collection.
  • remove(element) - removes the element; returns true if 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() - true when size() is 0.
  • clear() - removes every element.
  • clone() - returns a shallow copy of the Set.
  • equals(set) - true if 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() == 0

Apex 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.

Share this article