Avoiding SOQL 101 Error: Query Optimization Strategies
If you’ve spent any time writing Apex, you’ve probably run into it: System.LimitException: Too many SOQL queries: 101. It stops your code cold, usually at the worst possible time — during a data migration, a high-volume batch run, or when a key client is testing your new feature.
The SOQL 101 error is Salesforce’s governor limit enforcement telling you that your transaction attempted more than 100 SOQL queries. It’s one of the most common errors Salesforce developers face, and it’s almost always avoidable with the right patterns.
This guide walks you through why it happens, the exact mistakes that cause it, and — more importantly — how to fix it for good.
Table of Contents
- Understanding the SOQL 101 Error
- Common Mistakes Leading to SOQL 101
- Prevention Strategies
- Advanced Optimization Techniques
- Code Examples
- Using the Limits Class
- Monitoring SOQL Usage
- Debugging SOQL Limits
- FAQ
- Conclusion + Checklist
Understanding the SOQL 101 Error
What Causes This Error?
Salesforce enforces a hard limit of 100 synchronous SOQL queries per transaction. When your Apex code crosses that threshold, the runtime throws System.LimitException: Too many SOQL queries: 101 and rolls back the entire transaction.
This limit exists because Salesforce is a multi-tenant platform. Every org shares infrastructure, so governor limits prevent any single transaction from hogging database resources at the expense of everyone else.
The limit applies to all synchronous Apex contexts — triggers, controllers, service classes, and anything else running in a single transaction. Asynchronous contexts like @future methods, Queueable jobs, and Batch Apex each get their own governor limit allocation, which is why moving work async sometimes masks the underlying problem.

Read this complete documentation on Salesforce Governor Limits & Performance
Impact on Your Code
When a SOQL 101 error fires, your entire transaction fails and rolls back. That means no partial saves — every DML operation in that transaction is reversed.
Beyond the immediate failure, this error signals a deeper architectural problem. Code that hits SOQL limits usually means your logic isn’t designed with bulk processing in mind. A single-record test won’t catch it, but processing 200 records in a trigger batch will. The result is bugs that only appear in production, under real load.
Common Mistakes Leading to SOQL 101
SOQL in Loops
This is the #1 cause of the SOQL 101 error in Salesforce. Placing a SOQL query inside a for loop means you execute one query per iteration. Process 101 records, get 101 queries — instant limit breach.
Apex Code
// This will blow up on any significant data volume
for (Account acc : trigger.new) {
List<Contact> contacts = [SELECT Id, Name FROM Contact WHERE AccountId = :acc.Id];
// do something with contacts
}
Every Salesforce developer learns this pattern early, but it has a way of sneaking back in — especially in helper methods where the loop and the query are in different files.
Trigger Recursion
Trigger recursion happens when a trigger fires, performs a DML operation, which in turn fires another trigger, creating a chain reaction. Each re-entry into a trigger context can spawn fresh SOQL queries.
For example: an Account trigger updates related Contacts, which fires the Contact trigger, which queries back to Account. Even with careful logic, recursive patterns dramatically accelerate your query count.
A simple static Boolean flag — TriggerHelper.isRunning — breaks the recursion. More robust solutions use a set to track already-processed record IDs.
Inefficient Helper Methods
Helper and service classes are great for code organization, but they can hide expensive query patterns. A ContactService.getByAccount(accountId) method looks clean at the call site. If it executes a SOQL query every time it’s called, though, and you call it in a loop, you’ve hidden a SOQL 101 bomb in plain sight.
Always audit helper methods for query execution before calling them inside loops or recursive logic. If a method queries the database, it shouldn’t be called in a loop — full stop.
Prevention Strategies
Bulkifying Queries
Bulkification means writing code that processes a collection of records in a single operation rather than one record at a time. For SOQL, this means collecting all the IDs you need upfront, running one query against the full set, and processing results from memory.
This is the single most impactful change you can make to avoid too many SOQL queries in Apex. One query for 200 records is always better than 200 queries for 1 record each.
The rule is simple: collect, query once, iterate.
Using Collections
Collections — List, Set, and Map — are your best friends for SOQL optimization. Use a Set<Id> to collect record IDs before your query, then use the :idSet bind variable in your WHERE clause.
Apex Code
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : trigger.new) {
accountIds.add(opp.AccountId);
}
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
One query. One round trip to the database. No matter how many records are in trigger.new.
Query Outside Loops
The golden rule of Salesforce query optimization: never put a SOQL query inside a loop. This applies to for loops, while loops, and recursive method calls.
If you find yourself needing data from a related object while iterating records, extract the IDs first, query once, then build a Map for O(1) lookup inside the loop. We’ll cover that pattern in detail in the Advanced section below.
Advanced Optimization Techniques

Relationship Queries
Relationship queries (also called parent-to-child or child-to-parent queries) let you retrieve related records in a single SOQL statement rather than running separate queries for each object.
Parent-to-child (subquery):
Apex Code
List<Account> accounts = [
SELECT Id, Name,
(SELECT Id, Name, Email FROM Contacts)
FROM Account
WHERE Id IN :accountIds
];
Child-to-parent:
Apex Code
List<Contact> contacts = [
SELECT Id, Name, Account.Name, Account.Industry
FROM Contact
WHERE AccountId IN :accountIds
];
Using relationship queries correctly can collapse what would be multiple queries into a single statement. That’s often the difference between staying under the 100-query limit and blowing past it.
Using Maps for Lookups
After you’ve run a bulkified query, the next challenge is efficiently accessing the results when you iterate over your records. This is where Map<Id, SObject> becomes essential.
Apex Code
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]
);
for (Contact c : trigger.new) {
Account relatedAccount = accountMap.get(c.AccountId);
// instant lookup, no query needed
}
Map<Id, SObject> constructor syntax is one of Apex’s most useful shortcuts. It builds the map directly from a query result in one line, and gives you O(1) access to any record by ID.
Aggregate Queries
Aggregate queries — using COUNT(), SUM(), MAX(), MIN(), GROUP BY — let you compute summaries at the database level instead of loading records into memory and calculating in Apex.
If you need the count of Opportunities per Account, don’t query all Opportunities and loop through them counting in Apex. Do it in SOQL:
Apex Code
List<AggregateResult> results = [
SELECT AccountId, COUNT(Id) oppCount
FROM Opportunity
WHERE AccountId IN :accountIds
GROUP BY AccountId
];
One query replaces what could otherwise be dozens. Aggregate queries are underused by many developers and often provide significant wins for both query count and heap size.
Code Examples
Bad Code Examples
Here’s a classic SOQL 101 scenario in a trigger:
Apex Code
// ❌ BAD: SOQL inside a loop — will fail at 101 records
trigger OpportunityTrigger on Opportunity (after insert, after update) {
for (Opportunity opp : trigger.new) {
Account acc = [SELECT Id, Name, Industry FROM Account WHERE Id = :opp.AccountId];
if (acc.Industry == 'Technology') {
// do something
}
}
}
Process 101 Opportunities → 101 SOQL queries → System.LimitException. Every time.
Here’s a subtler version in a service class:
Apex Code
// ❌ BAD: Hidden SOQL in helper called from a loop
public class ContactService {
public static List<Contact> getByAccount(Id accountId) {
return [SELECT Id, Name FROM Contact WHERE AccountId = :accountId]; // query here!
}
}
// In trigger:
for (Account acc : trigger.new) {
List<Contact> contacts = ContactService.getByAccount(acc.Id); // 1 query per account
}
The query is hidden in the service method, making the problem easy to miss in code review.
Optimized Code Examples
Here’s the same trigger, rewritten correctly:
Apex Code
// ✅ GOOD: Bulkified trigger with map-based lookup
trigger OpportunityTrigger on Opportunity (after insert, after update) {
// Step 1: Collect all Account IDs
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : trigger.new) {
if (opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
// Step 2: Single query for all related Accounts
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]
);
// Step 3: Iterate using map lookup — zero additional queries
for (Opportunity opp : trigger.new) {
Account acc = accountMap.get(opp.AccountId);
if (acc != null && acc.Industry == 'Technology') {
// do something
}
}
}
One query. Handles 1 record or 10,000 records equally well.
And the refactored service class:
Apex Code
// ✅ GOOD: Bulkified service method accepts a Set of IDs
public class ContactService {
public static Map<Id, List<Contact>> getByAccounts(Set<Id> accountIds) {
Map<Id, List<Contact>> result = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, Name, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!result.containsKey(c.AccountId)) {
result.put(c.AccountId, new List<Contact>());
}
result.get(c.AccountId).add(c);
}
return result;
}
}
The caller collects all account IDs first, calls the method once, and gets back a map for O(1) lookup. One query regardless of input size.
Using the Limits Class
Salesforce’s Limits class gives you real-time visibility into your governor limit consumption. It’s essential for building defensively and for debugging limit issues.
Monitoring SOQL Usage
Two methods are most useful for SOQL tracking:
Apex Code
// How many SOQL queries have been executed so far?
Integer queriesUsed = Limits.getQueries();
// What's the maximum allowed?
Integer queriesLimit = Limits.getLimitQueries(); // returns 100
// Remaining headroom
Integer remaining = queriesLimit - queriesUsed;
System.debug('SOQL queries used: ' + queriesUsed + ' / ' + queriesLimit);
You can embed these checks in service classes to log usage or throw early, descriptive exceptions when approaching the limit:
Apex Code
public static void checkQueryBudget(Integer budgetRequired) {
Integer remaining = Limits.getLimitQueries() - Limits.getQueries();
if (remaining < budgetRequired) {
throw new LimitException(
'Insufficient SOQL budget. Required: ' + budgetRequired +
', Remaining: ' + remaining
);
}
}
This technique is especially useful in complex service architectures where multiple classes contribute to query consumption in a single transaction.
Debugging SOQL Limits
When a SOQL 101 error fires in production, here’s how to track it down quickly.
1. Check the Debug Log. The Salesforce Developer Console’s log inspector shows every SOQL query execution with its line number and execution count. Filter for SOQL_EXECUTE_BEGIN events to see exactly what’s running and how often.
2. Use Apex Replay Debugger. VS Code’s Apex Replay Debugger lets you step through a debug log as if it were a live debug session. This is the fastest way to pinpoint which line in a complex codebase is triggering repeated queries.
3. Add Limits.getQueries() instrumentation. Drop System.debug(Limits.getQueries()) at key points in your code path to see how your query count accumulates through each step of execution.
4. Check for hidden queries in managed packages. Third-party managed packages consume from the same governor limit pool as your custom code. If you’re calling into a managed package mid-transaction, it may be consuming queries you didn’t account for.
5. Review recursive trigger flows. If the error appears intermittently at specific data volumes, check for trigger recursion. Add recursion guards and log entry/exit points in your trigger handler.
Pro Tip: Salesforce’s Event Monitoring (available in higher-tier orgs) logs Apex execution details including SOQL counts. It’s invaluable for diagnosing intermittent limit errors in production without being able to reproduce them locally.
Frequently Asked Questions
Q: What is the SOQL 101 error in Salesforce?
The SOQL 101 error (System.LimitException: Too many SOQL queries: 101) fires when Apex code executes more than 100 SOQL queries in a single synchronous transaction. It’s a governor limit enforcement and rolls back the entire transaction when triggered.
Q: How do I fix “too many SOQL queries” in Apex?
The core fix is bulkification: collect all the record IDs you need before looping, run a single SOQL query against the full set, store results in a Map<Id, SObject>, then iterate using map lookups. Never place SOQL queries inside for loops.
Q: Does the 100 SOQL limit apply to async Apex?
Each asynchronous context (Queueable, @future, Batch Apex execute method) gets its own fresh governor limit allocation — 100 SOQL queries per context. This is why moving logic to async can sometimes work around the error, but it doesn’t fix the underlying inefficiency.
Q: How can I monitor SOQL usage in my code?
Use Limits.getQueries() to get the current query count and Limits.getLimitQueries() to get the max (100). You can add assertions or debug statements at key points to track consumption through complex transactions.
Q: Can relationship queries (subqueries) help avoid SOQL 101?
Yes. A parent-to-child subquery counts as one SOQL query but returns related child records alongside parent records. Using subqueries instead of separate queries for related objects is one of the most effective ways to reduce total query count.
Q: What’s the SOQL query limit in asynchronous Batch Apex?
Each execution of a Batch Apex execute() method gets 100 SOQL queries. The start() method gets 100, and finish() gets 100. Each is an independent transaction context with its own limit.
Q: Do SOQL queries in managed packages count toward my limit?
Yes. All SOQL queries in a transaction — including those inside managed packages you call — count toward the same 100-query limit. Factor this in when designing integrations that chain your code with managed package logic.
Conclusion + Checklist
The SOQL 101 error is almost always a design problem, not a Salesforce problem. The platform’s governor limits push you toward scalable patterns — collect IDs upfront, query once, use maps for lookups. These patterns don’t just avoid errors; they write code that performs correctly at any data volume.
The developers who rarely hit SOQL limits aren’t doing anything magical. They’ve internalized a few core rules and apply them consistently. With practice, bulkifying SOQL becomes second nature.