In This Guide
1 What is SOQL?
SOQL stands for Salesforce Object Query Language. It is the query language used to retrieve records from the Salesforce database, both from standard objects (like Account, Contact, Opportunity) and custom objects you create in your org. SOQL is modelled closely after SQL and will feel familiar if you have worked with relational databases, but it has important differences that reflect Salesforce's multi-tenant, cloud architecture.
SOQL vs SQL — Key Differences
| Feature | SQL | SOQL |
|---|---|---|
| Read data | SELECT | SELECT (same) |
| Modify data | INSERT, UPDATE, DELETE | Not supported — use DML |
| Wildcard SELECT | SELECT * | Must name every field |
| Joins | JOIN keyword | Relationship traversal (dot notation / subquery) |
| Date filtering | Date literals vary by DB | Built-in: TODAY, THIS_MONTH, LAST_N_DAYS:n |
| Execution limit | None (database level) | 100 queries per Apex transaction |
| Row limit | Varies | 50,000 rows returned per query; 10,000 via getQueryLocator |
When to Use SOQL
Use SOQL whenever you need to retrieve records from Salesforce in Apex, Flows (via Get Records element), or the Developer Console. SOQL is the only way to query the Salesforce database from code. To write or delete records you use Data Manipulation Language (DML) statements — insert, update, delete, upsert — which are separate from SOQL entirely.
Developer Console tip: You can execute SOQL queries interactively in the Salesforce Developer Console via Query Editor, or in VS Code with the Salesforce CLI command sf data query --query "SELECT Id FROM Account LIMIT 5".
2 Basic Syntax — SELECT Fields FROM Object
Every SOQL query follows this fundamental pattern:
SELECT Field1, Field2, Field3
FROM ObjectApiName
WHERE Condition
ORDER BY FieldName ASC|DESC
LIMIT 200
Let's look at real, practical examples starting from the simplest query and building up.
Example 1 — Retrieve All Accounts (with a safety LIMIT)
SELECT Id, Name, Industry, AnnualRevenue, Phone
FROM Account
LIMIT 200
Example 2 — Query a Custom Object
Custom objects always end in __c. Custom fields also use the __c suffix.
SELECT Id, Name, Project_Status__c, Budget__c, Deadline__c
FROM Project__c
WHERE Project_Status__c = 'Active'
ORDER BY Deadline__c ASC
LIMIT 50
Example 3 — Querying Contacts with Multiple Fields
SELECT Id, FirstName, LastName, Email, Title, Department,
AccountId, Account.Name, LeadSource
FROM Contact
WHERE Email != null
ORDER BY LastName ASC, FirstName ASC
LIMIT 500
Important: SOQL does not support SELECT *. You must always explicitly list the fields you want. Querying only the fields you need reduces heap size and makes your code more efficient.
3 WHERE Clause — Operators & Date Literals
The WHERE clause filters which records are returned. SOQL supports a rich set of comparison and logical operators.
Comparison Operators
| Operator | Description | Example |
|---|---|---|
| = | Equal to | Stage = 'Closed Won' |
| != | Not equal to | Status != 'Inactive' |
| < | Less than | Amount < 10000 |
| > | Greater than | Amount > 50000 |
| <= | Less than or equal | CloseDate <= TODAY |
| >= | Greater than or equal | CreatedDate >= LAST_MONTH |
| LIKE | Pattern match (% wildcard) | Name LIKE '%acme%' |
| IN | Matches any value in set | Stage IN ('Prospecting','Qualification') |
| NOT IN | Does not match any value | LeadSource NOT IN ('Web','Email') |
| INCLUDES | Multi-select picklist includes | Languages__c INCLUDES ('English') |
| EXCLUDES | Multi-select picklist excludes | Languages__c EXCLUDES ('French') |
Logical Operators — AND, OR, NOT
-- AND: both conditions must be true
SELECT Id, Name, Amount
FROM Opportunity
WHERE StageName = 'Closed Won'
AND Amount > 100000
AND CloseDate = THIS_YEAR
-- OR: at least one condition must be true
SELECT Id, Name, Industry
FROM Account
WHERE Industry = 'Technology'
OR Industry = 'Finance'
-- NOT: negates a condition
SELECT Id, LastName, Email
FROM Contact
WHERE NOT Email = null
LIKE Operator and Wildcards
The LIKE operator performs case-insensitive pattern matching. The % wildcard matches any sequence of characters; _ matches any single character.
-- Starts with 'Acme'
SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%'
-- Contains 'cloud' anywhere in the name
SELECT Id, Name FROM Account WHERE Name LIKE '%cloud%'
-- Email domain matches (ends with @salesforce.com)
SELECT Id, Email FROM Contact
WHERE Email LIKE '%@salesforce.com'
Date Literals
Salesforce provides powerful built-in date literals that automatically adjust relative to the current date and the running user's timezone. No date formatting or conversion is required.
| Date Literal | Matches Records Where… |
|---|---|
| TODAY | Date = today |
| YESTERDAY | Date = yesterday |
| TOMORROW | Date = tomorrow |
| THIS_WEEK | Date falls in the current week (Sun–Sat) |
| LAST_WEEK | Date falls in last week |
| THIS_MONTH | Date falls in the current calendar month |
| LAST_MONTH | Date falls in the previous calendar month |
| THIS_YEAR | Date falls in the current year |
| LAST_N_DAYS:n | Date is within the last n days (including today) |
| NEXT_N_DAYS:n | Date is within the next n days |
| LAST_N_MONTHS:n | Date is within the last n calendar months |
| NEXT_N_WEEKS:n | Date is within the next n weeks |
-- Opportunities closing in the next 30 days
SELECT Id, Name, StageName, CloseDate, Amount
FROM Opportunity
WHERE CloseDate = NEXT_N_DAYS:30
AND IsClosed = false
-- Cases created in the last 7 days that are still open
SELECT Id, CaseNumber, Subject, Status, CreatedDate
FROM Case
WHERE CreatedDate >= LAST_N_DAYS:7
AND Status != 'Closed'
-- Leads created this month
SELECT Id, FirstName, LastName, Company, LeadSource
FROM Lead
WHERE CreatedDate = THIS_MONTH
ORDER BY CreatedDate DESC
Tip: Use LAST_N_DAYS:n with a colon and no spaces around it. The value n must be a positive integer and cannot be a variable — use Apex binding for dynamic ranges.
4 Relationships — Parent-to-Child & Child-to-Parent
Salesforce objects are connected through lookup and master-detail relationships. SOQL lets you traverse these relationships without needing SQL JOINs — using either dot notation (child to parent) or subqueries (parent to child).
Child-to-Parent — Dot Notation
When you are querying a child object and want to access fields on its parent, use dot notation to walk up the relationship. The relationship name is the field without the "Id" suffix (e.g., AccountId becomes Account).
-- Query Contacts and include their parent Account's name and industry
SELECT Id, FirstName, LastName, Email,
Account.Name, -- parent Account Name
Account.Industry, -- parent Account Industry
Account.BillingCity
FROM Contact
WHERE Account.Industry = 'Technology'
-- Query Opportunities and walk up to the Account's Owner
SELECT Id, Name, StageName, Amount,
Account.Name,
Account.Owner.Name, -- two hops up the relationship
Account.Owner.Email
FROM Opportunity
WHERE StageName = 'Closed Won'
AND CloseDate = THIS_YEAR
-- Custom relationship: Case with custom lookup to Product__c
SELECT Id, Subject, Related_Product__r.Name, Related_Product__r.Price__c
FROM Case
WHERE Related_Product__r.IsActive__c = true
Custom relationship fields: For custom lookup fields, the relationship name uses __r instead of __c. So a field named Related_Product__c becomes Related_Product__r when traversing the relationship.
Parent-to-Child — Subquery
To retrieve a parent record along with its related child records, use a subquery in the SELECT clause. The subquery uses the child relationship name (usually the plural of the child object). You can find this name in the object's relationship section in Setup.
-- Accounts with their related Contacts
SELECT Id, Name, Industry,
(SELECT Id, FirstName, LastName, Email
FROM Contacts) -- 'Contacts' is the child relationship name
FROM Account
WHERE Industry = 'Technology'
LIMIT 50
-- Accounts with their Opportunities that closed this year
SELECT Id, Name,
(SELECT Id, Name, StageName, Amount, CloseDate
FROM Opportunities
WHERE StageName = 'Closed Won'
AND CloseDate = THIS_YEAR
ORDER BY Amount DESC)
FROM Account
WHERE Name LIKE '%Acme%'
-- Custom parent-to-child: Project__c with its Tasks__r
SELECT Id, Name, Project_Status__c,
(SELECT Id, Name, Due_Date__c, Assigned_To__r.Name
FROM Tasks__r -- __r for custom relationships
WHERE Is_Complete__c = false)
FROM Project__c
Accessing Subquery Results in Apex
List<Account> accounts = [
SELECT Id, Name,
(SELECT Id, LastName, Email FROM Contacts)
FROM Account
WHERE Industry = 'Technology'
LIMIT 20
];
for (Account acc : accounts) {
// getSObjects() returns the child list — may be null
List<Contact> contacts = acc.getSObjects('Contacts');
if (contacts != null) {
for (Contact c : contacts) {
System.debug(acc.Name + ' — ' + c.LastName);
}
}
}
5 Aggregate Functions — COUNT, SUM, AVG, MIN, MAX
SOQL supports five aggregate functions that let you perform calculations on groups of records. When aggregate functions are used, SOQL returns AggregateResult objects rather than SObject records.
Available Aggregate Functions
| Function | Description | Applies To |
|---|---|---|
| COUNT() | Count of all rows (including nulls) | Any field or * |
| COUNT(field) | Count of non-null values for a field | Any field |
| COUNT_DISTINCT(field) | Count of distinct non-null values | Any field |
| SUM(field) | Sum of numeric field values | Number, Currency, Percent |
| AVG(field) | Average of numeric field values | Number, Currency, Percent |
| MIN(field) | Minimum value | Number, Date, DateTime, Currency |
| MAX(field) | Maximum value | Number, Date, DateTime, Currency |
GROUP BY — Grouping Results
Use GROUP BY with aggregate functions to compute statistics for each unique value of a field. All non-aggregate fields in the SELECT must appear in the GROUP BY clause.
-- Count of Opportunities by Stage
SELECT StageName, COUNT(Id) opptyCount
FROM Opportunity
GROUP BY StageName
-- Total and average opportunity amount by Account
SELECT AccountId, Account.Name,
SUM(Amount) totalRevenue,
AVG(Amount) avgDeal,
COUNT(Id) dealCount,
MAX(Amount) largestDeal
FROM Opportunity
WHERE StageName = 'Closed Won'
AND CloseDate = THIS_YEAR
GROUP BY AccountId, Account.Name
ORDER BY SUM(Amount) DESC
LIMIT 20
-- Cases per priority this month
SELECT Priority, COUNT(Id) caseCount,
MIN(CreatedDate) earliestCase
FROM Case
WHERE CreatedDate = THIS_MONTH
GROUP BY Priority
HAVING — Filtering Grouped Results
The HAVING clause filters groups after aggregation, similar to WHERE but applied to aggregate values. You cannot use WHERE to filter on an aggregate function result.
-- Accounts with more than 10 open opportunities
SELECT AccountId, COUNT(Id) openDeals
FROM Opportunity
WHERE IsClosed = false
GROUP BY AccountId
HAVING COUNT(Id) > 10
-- Product categories with total sales above $500,000
SELECT Category__c, SUM(Amount__c) totalSales
FROM Order_Line__c
GROUP BY Category__c
HAVING SUM(Amount__c) > 500000
ORDER BY SUM(Amount__c) DESC
Reading AggregateResult in Apex
List<AggregateResult> results = [
SELECT StageName, COUNT(Id) cnt, SUM(Amount) total
FROM Opportunity
WHERE CloseDate = THIS_YEAR
GROUP BY StageName
];
for (AggregateResult ar : results) {
String stage = (String) ar.get('StageName');
Integer count = (Integer) ar.get('cnt');
Decimal total = (Decimal) ar.get('total');
System.debug(stage + ': ' + count + ' deals — $' + total);
}
Aliasing: You can give aggregate functions an alias (e.g., COUNT(Id) cnt) and then retrieve the value with ar.get('cnt'). Without an alias, use ar.get('expr0'), ar.get('expr1'), etc.
6 ORDER BY, LIMIT, and OFFSET
These three clauses control the ordering and pagination of your query results.
ORDER BY
Sort results by one or more fields. Use ASC (ascending, default) or DESC (descending). Handle null values with NULLS FIRST or NULLS LAST.
-- Sort by close date descending, then by amount descending
SELECT Id, Name, CloseDate, Amount
FROM Opportunity
ORDER BY CloseDate DESC, Amount DESC
LIMIT 100
-- Put records where Email is null at the end
SELECT Id, LastName, Email
FROM Contact
ORDER BY Email ASC NULLS LAST
LIMIT
LIMIT restricts the maximum number of rows returned. Always include a LIMIT in ad-hoc queries and set a sensible ceiling in production code to avoid hitting the 50,000 row governor limit.
OFFSET — Pagination
OFFSET skips a specified number of records at the start of the result set. Combined with LIMIT, it enables page-by-page data retrieval. The maximum OFFSET value is 2,000.
-- Page 1: records 1–20
SELECT Id, Name, Industry FROM Account
ORDER BY Name ASC
LIMIT 20 OFFSET 0
-- Page 2: records 21–40
SELECT Id, Name, Industry FROM Account
ORDER BY Name ASC
LIMIT 20 OFFSET 20
-- Page 3: records 41–60
SELECT Id, Name, Industry FROM Account
ORDER BY Name ASC
LIMIT 20 OFFSET 40
OFFSET limitation: OFFSET is capped at 2,000. For querying large data sets beyond that limit, use cursor-based pagination with Database.getQueryLocator() inside a Batch Apex class, which handles up to 50 million records.
7 Semi-joins and Anti-joins
SOQL supports using a subquery inside an IN or NOT IN clause to filter records based on the existence (or absence) of related records. These are called semi-joins and anti-joins respectively.
Semi-join — IN with Subquery
A semi-join returns records where a field value matches the result of an inner subquery. The inner query must return a single Id field.
-- Contacts whose Account has at least one Closed Won Opportunity
SELECT Id, FirstName, LastName, Email
FROM Contact
WHERE AccountId IN (
SELECT AccountId
FROM Opportunity
WHERE StageName = 'Closed Won'
)
-- Users who are members of a specific Permission Set
SELECT Id, Name, Email
FROM User
WHERE Id IN (
SELECT AssigneeId
FROM PermissionSetAssignment
WHERE PermissionSet.Name = 'Salesforce_API_Access'
)
Anti-join — NOT IN with Subquery
An anti-join returns records that do not have related records matching the inner query — the inverse of a semi-join.
-- Accounts that have NO Opportunities at all
SELECT Id, Name, Industry, CreatedDate
FROM Account
WHERE Id NOT IN (
SELECT AccountId
FROM Opportunity
WHERE AccountId != null
)
-- Contacts who have no open Cases
SELECT Id, Name, Email
FROM Contact
WHERE Id NOT IN (
SELECT ContactId
FROM Case
WHERE Status != 'Closed'
AND ContactId != null
)
Performance note: Semi-joins and anti-joins that return very large inner sets can be slow. Where possible, filter the inner query tightly or use an alternative approach with Apex collections.
8 SOQL in Apex — Binding Variables & For Loops
SOQL is embedded directly inside Apex code using inline query syntax surrounded by square brackets. This inline SOQL is validated at compile time, giving you early error detection.
Inline SOQL and Variable Binding
You can bind Apex variables directly into a SOQL query using the colon (:) prefix. Bound variables are automatically escaped, which prevents SOQL injection attacks — never concatenate user-supplied strings into queries.
// Single variable binding
String targetIndustry = 'Technology';
List<Account> techAccounts = [
SELECT Id, Name, Phone
FROM Account
WHERE Industry = :targetIndustry
ORDER BY Name
LIMIT 200
];
// List/Set binding — great for IN clauses
Set<Id> accountIds = new Set<Id>{'001xx000001A2bCAAE', '001xx000001B3cDAAE'};
List<Account> specificAccounts = [
SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Id IN :accountIds
];
// Integer/Decimal binding
Decimal minimumAmount = 50000;
Date cutoffDate = Date.today().addDays(-30);
List<Opportunity> bigRecentDeals = [
SELECT Id, Name, Amount, CloseDate, StageName
FROM Opportunity
WHERE Amount >= :minimumAmount
AND CloseDate >= :cutoffDate
ORDER BY Amount DESC
];
SOQL For Loops — Memory-Efficient Processing
A SOQL for loop retrieves records in chunks of 200 and processes them without loading all records into heap memory at once. This is the preferred pattern when processing large result sets in Apex, as it respects heap size limits.
// Pattern 1: Iterate one record at a time (200 fetched per batch internally)
for (Contact c : [
SELECT Id, Email, DoNotContact__c
FROM Contact
WHERE HasOptedOutOfEmail = false
]) {
// process each contact
c.Last_Contacted__c = Date.today();
}
// Pattern 2: Chunked list — process 200 records at a time explicitly
List<Account> toUpdate = new List<Account>();
for (List<Account> chunk : [
SELECT Id, Name, Rating
FROM Account
WHERE Industry = 'Technology'
]) {
for (Account acc : chunk) {
acc.Rating = 'Hot';
toUpdate.add(acc);
}
if (toUpdate.size() >= 200) {
update toUpdate;
toUpdate.clear();
}
}
if (!toUpdate.isEmpty()) { update toUpdate; }
Database.query() — Dynamic SOQL
When you need to build a query dynamically at runtime (e.g., user-selected filters), use Database.query(). Be very careful to sanitise any user-supplied input using String.escapeSingleQuotes() to prevent SOQL injection.
String objectName = 'Account';
String filterField = 'Industry';
// ALWAYS escape user-supplied strings
String filterValue = String.escapeSingleQuotes(userInput);
String soql = 'SELECT Id, Name FROM '
+ objectName
+ ' WHERE ' + filterField
+ ' = \'' + filterValue + '\' LIMIT 200';
List<SObject> results = Database.query(soql);
9 Best Practices
Writing efficient, safe SOQL is essential for building applications that perform well within Salesforce's governor limits. Follow these practices consistently.
1. Never Put SOQL Inside a Loop
This is the single most common governor limit violation. Each SOQL call inside a loop counts against the 100 SOQL queries per transaction limit. Instead, collect all the IDs or criteria first, then run one query and map results.
Running a SOQL query inside a for loop over records burns through governor limits rapidly and will throw a LimitException with large data volumes.
Collect all needed IDs into a Set before the loop, run one SOQL query with WHERE Id IN :idSet, build a Map, and access it inside the loop in O(1) time.
// BAD: SOQL inside a loop — hits governor limits fast
for (Opportunity opp : opportunities) {
Account acc = [SELECT Id, Name FROM Account WHERE Id = :opp.AccountId]; // WRONG
}
// GOOD: One query outside the loop, Map lookup inside
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : opportunities) {
accountIds.add(opp.AccountId);
}
Map<Id, Account> accountMap = new Map<Id, Account>([
SELECT Id, Name, Industry
FROM Account
WHERE Id IN :accountIds
]);
for (Opportunity opp : opportunities) {
Account acc = accountMap.get(opp.AccountId); // safe O(1) lookup
}
2. Use WITH SECURITY_ENFORCED
Adding WITH SECURITY_ENFORCED to a query ensures that the running user's field-level security (FLS) and object permissions are respected. If the user does not have access to a queried field, a System.QueryException is thrown. This is especially important in Visualforce pages and LWC components that run in user context.
SELECT Id, Name, Salary__c
FROM Employee__c
WITH SECURITY_ENFORCED
WHERE Department__c = 'Engineering'
LIMIT 100
3. Always Use LIMIT
Always add a LIMIT to queries in production code. Without it, a query could return tens of thousands of rows, consuming heap memory and time. Use Database.getQueryLocator() in Batch Apex for processing millions of records.
4. Selective Queries — Use Indexed Fields in WHERE
Salesforce automatically indexes Id, Name, OwnerId, CreatedDate, SystemModstamp, and foreign key fields (lookups and master-detail). Filtering on indexed fields produces selective queries that run much faster. Non-selective queries on large objects can time out.
5. Query Only the Fields You Need
Avoid retrieving unnecessary fields. Every extra field increases heap size consumption. On objects with very wide field definitions, querying all fields can significantly impact performance and push you toward heap limits.
6. Use Apex Binding Instead of String Concatenation
Always use the colon binding syntax (WHERE Name = :myVar) in static SOQL. For dynamic SOQL, always call String.escapeSingleQuotes() on any user-supplied or external input before injecting it into a query string.
10 Common Mistakes to Avoid
Mistake 1 — Querying in a Loop (SOQL in Loops)
Already covered in best practices, but worth repeating: putting a SOQL query inside any type of loop (for, while, do-while) will throw a System.LimitException: Too many SOQL queries: 101 error once the loop exceeds 100 iterations in a single transaction.
Mistake 2 — Forgetting to Handle Empty Results
When querying for a single record using a query that might return zero rows, do not assume a result exists. Wrap single-record inline queries in a try-catch or use a List and check its size.
// RISKY: throws QueryException if no record found
Account acc = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];
// SAFE: use a List, check size first
List<Account> accs = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];
if (!accs.isEmpty()) {
Account acc = accs[0];
// safe to use acc here
}
Mistake 3 — Using SELECT * (Wildcard)
SOQL does not support SELECT *. You will get a compile-time error. You must explicitly list every field. If you need all fields dynamically, use Schema.SObjectType.Account.fields.getMap().keySet() to build the field list at runtime.
Mistake 4 — Not Filtering Subquery Results for Null
When using anti-joins (NOT IN with a subquery), if the inner subquery field can be null, a null value in the inner result set will cause the outer query to return zero records. Always add a WHERE fieldName != null filter inside the inner subquery.
Mistake 5 — Ignoring Governor Limits in Async Context
Batch Apex, Queueable, and Scheduled Apex each have their own governor limit context. Batch Apex execute() allows 200 SOQL queries per chunk, but each execute() call is a separate transaction. Always design your queries with these limits in mind, and use Limits.getQueries() during development to monitor consumption.
Mistake 6 — Non-Selective Queries on Large Objects
Querying a large object (over 1 million records) without a selective WHERE clause — for example, filtering on a non-indexed custom text field — can time out or produce a "System.QueryException: Non-selective query against large object type" error. Always filter on indexed fields, use CreatedDate ranges, or add a custom index via Salesforce support.
Practice SOQL with Real Challenges
Reinforce what you've learned by solving hands-on SOQL problems on ApexArena. No org required — run queries in your browser and get instant feedback.
Filter Closed Won Opportunities
Write a SOQL query that returns all Closed Won opportunities for the current year, ordered by amount descending.
Account Revenue Aggregation
Use GROUP BY and SUM to calculate total Opportunity revenue per Account. Return only Accounts with more than $100,000 in revenue.
Contacts with No Open Cases
Write an anti-join SOQL query that returns all Contacts who currently have no open Cases linked to them.
Bulkified Apex Trigger
Refactor a trigger handler that has SOQL inside a loop. Use a Map-based pattern to process 200 records with a single query.
Frequently Asked Questions
What is SOQL in Salesforce?
SOQL (Salesforce Object Query Language) is a SELECT-only query language used to retrieve records from the Salesforce database. Unlike SQL, it cannot perform INSERT, UPDATE, DELETE, or JOIN operations directly. It is purpose-built to query Salesforce objects (standard and custom) and navigate object relationships using dot notation and subqueries. SOQL is used inside Apex code, Flows, REST API requests, and the Developer Console.
How do I write a basic SOQL query?
A basic SOQL query follows this pattern: SELECT Field1, Field2 FROM ObjectApiName WHERE Condition ORDER BY Field LIMIT n. For example: SELECT Id, Name, Industry FROM Account WHERE Industry = 'Technology' ORDER BY Name LIMIT 50. You must always specify at least one field — SOQL does not support SELECT *. The WHERE, ORDER BY, and LIMIT clauses are all optional but recommended for production code.
What is the difference between SOQL and SQL?
SOQL is modelled after SQL but has key differences: (1) SOQL is read-only — no INSERT, UPDATE, or DELETE (use DML statements for those); (2) SOQL does not support wildcards like SELECT *; (3) SOQL uses relationship traversal (dot notation and subqueries) instead of JOIN; (4) SOQL has Salesforce-specific date literals like TODAY, LAST_N_DAYS:7, and THIS_MONTH; (5) SOQL runs inside Apex and enforces governor limits including a maximum of 100 SOQL queries per transaction and 50,000 rows returned per query.
How do I avoid SOQL in loops in Apex?
Move all SOQL queries outside any loop. Collect the IDs or values you need while iterating first, then run a single bulkified query after the loop. Store results in a Map<Id, SObject> for O(1) access. For example, instead of querying inside a for loop over Opportunity records, first collect all AccountIds into a Set, then run new Map<Id, Account>([SELECT Id, Name FROM Account WHERE Id IN :accountIds]) and access accountMap.get(opp.AccountId) inside the loop.