Top 40 Salesforce SOQL Interview Questions and Answers 2026 — Aggregate Queries, Relationships, Governor Limits & SOSL Explained with Apex Code
📅 Soql
🗄️ SOQL
Salesforce SOQL Interview Questions — Queries, Aggregates & Relationships Explained
SELECT, GROUP BY, HAVING, Date Literals, Sub-Queries & Relationship Traversal — Beginner to Intermediate
Beginner
Intermediate
13 Questions
Questions Index
1Retrieve Account Names & Industry
2Total Opportunities per Account
3Contacts on "XYZ Opportunity"
4All Closed Won Opportunities
55 Most Recently Created Leads
6Account Names with Opps > $50,000
7Average Amount of Closed Won Opps
8Opportunities Closing in Next 7 Days
9Contacts on Proposal Stage Opps
10Account with Maximum Opportunities
11Accounts with Exactly 2 Contacts
12Full Account Record — Max Opportunities
13Latest 10 Account Records
Q
Question 01 · 🟢 Basic
Retrieve all Account names and their corresponding Industry from Salesforce.
✅ Answer
Use a basic SELECT statement. SOQL does NOT support
SELECT * — field names must always be explicitly specified! 📋📌 Query
SELECT Name, Industry
FROM Account
ORDER BY Name ASC
📋 Breaking It Down
| Clause | Purpose |
|---|---|
| SELECT Name, Industry | Fetch only these two fields from the Account object |
| FROM Account | Target the standard Account sObject |
| ORDER BY Name ASC | Sort results alphabetically by Account name |
⚠️ No SELECT * in SOQL. Writing
SELECT * FROM Account will throw an error in Salesforce. Always use field API names explicitly.💻 In Apex
List<Account> accounts = [
SELECT Name, Industry
FROM Account
ORDER BY Name ASC
];
for (Account acc : accounts) {
System.debug(acc.Name + ' | ' + acc.Industry);
}
🎤 One-Line Answer for Interview
"I use SOQL's SELECT statement to query the Name and Industry fields from the Account object — SOQL doesn't support SELECT *, so field names must always be explicitly specified."
Q
Question 02 · 🟠 Intermediate
Find the total number of Opportunities for each Account.
✅ Answer
Use COUNT(Id) with GROUP BY. In Apex, aggregate queries return
AggregateResult — not a standard sObject list! 📊📌 Query
SELECT AccountId, Account.Name, COUNT(Id) total
FROM Opportunity
GROUP BY AccountId, Account.Name
ORDER BY COUNT(Id) DESC
📋 Breaking It Down
| Clause | Purpose |
|---|---|
| COUNT(Id) | Counts total Opportunities for each Account |
| GROUP BY AccountId, Account.Name | Required when using any aggregate function |
| ORDER BY COUNT(Id) DESC | Shows Account with highest Opportunities first |
⚠️ Golden Rule: Every non-aggregate field in SELECT must also be in GROUP BY. Use
HAVING (not WHERE) to filter on aggregated results — e.g. HAVING COUNT(Id) > 5💻 In Apex
List<AggregateResult> results = [
SELECT AccountId, Account.Name, COUNT(Id) total
FROM Opportunity
GROUP BY AccountId, Account.Name
];
for (AggregateResult ar : results) {
System.debug(ar.get('Name') + ' → ' + ar.get('total'));
}
🎤 One-Line Answer for Interview
"I use COUNT(Id) with GROUP BY AccountId on the Opportunity object to get total Opportunities per Account — and since it's an aggregate query, the Apex return type is AggregateResult, not a standard sObject list."
Q
Question 03 · 🟠 Intermediate
Retrieve the names of Contacts associated with an Opportunity named "XYZ Opportunity."
✅ Answer
Contact and Opportunity are NOT directly related — they are connected through the standard junction object OpportunityContactRole! 🔗
🔗 Relationship Chain: Opportunity ──── OpportunityContactRole ──── Contact
📌 Approach 1 — Query from OCR (Most Common)
SELECT Contact.Name, Contact.Email, Role
FROM OpportunityContactRole
WHERE Opportunity.Name = 'XYZ Opportunity'
📌 Approach 2 — Unique Contacts Only (No Duplicates)
SELECT Name, Email
FROM Contact
WHERE Id IN (
SELECT ContactId
FROM OpportunityContactRole
WHERE Opportunity.Name = 'XYZ Opportunity'
)
🔑 Approach 1 vs Approach 2
- 💡Approach 1 returns one row per OCR record — same Contact appears multiple times if linked to multiple Opportunities
- 💡Approach 2 returns unique Contacts only by querying the Contact object directly via sub-query
🎤 One-Line Answer for Interview
"Contact and Opportunity are not directly related — they are linked through the standard junction object OpportunityContactRole. So I query OpportunityContactRole and traverse the relationship to Contact.Name using dot notation."
Q
Question 04 · 🟢 Basic
List all Closed Won Opportunities along with their amounts.
✅ Answer
Filter using WHERE StageName = 'Closed Won'. StageName is a picklist field and the value is case-sensitive! 🎯
📌 Query
SELECT Name, Amount, StageName, CloseDate, Account.Name
FROM Opportunity
WHERE StageName = 'Closed Won'
ORDER BY Amount DESC
⚠️
'closed won' or 'Closed won' will return ZERO records. Must be exactly 'Closed Won'.📋 Opportunity Stage Reference
| StageName | IsClosed | IsWon | Meaning |
|---|---|---|---|
| Prospecting | false | false | Early stage lead |
| Proposal/Price Quote | false | false | Quote shared |
| Closed Won | ✅ true | ✅ true | Deal won |
| Closed Lost | ✅ true | ❌ false | Deal lost |
💡
IsClosed = true returns BOTH Closed Won AND Closed Lost. Use StageName = 'Closed Won' specifically for only won deals.🎤 One-Line Answer for Interview
"I filter the Opportunity object using WHERE StageName = 'Closed Won' — StageName is a picklist field and the value is case-sensitive. IsClosed = true returns both Closed Won and Closed Lost, so for only won deals, StageName filter is the right approach."
Q
Question 05 · 🟢 Basic
Identify the five most recently created Leads.
✅ Answer
Use ORDER BY CreatedDate DESC + LIMIT 5. Forgetting
DESC returns the OLDEST 5 leads — ASC is the default! 📅📌 Query
SELECT Name, Email, Phone, Company, LeadSource, Status, CreatedDate
FROM Lead
ORDER BY CreatedDate DESC
LIMIT 5
📋 LIMIT — Key Rules
| Scenario | Behaviour |
|---|---|
| LIMIT 1 | Exactly 1 record — used when you expect a unique result |
| LIMIT 200 | Default max records shown in Developer Console |
| LIMIT 50000 | Maximum records a single SOQL can return in Apex |
💡 CreatedDate vs LastModifiedDate:
CreatedDate is set ONCE when record is created — never changes. LastModifiedDate updates every time the record is edited.⚠️ Always pair
LIMIT with ORDER BY for predictable results — without ORDER BY, LIMIT picks records in arbitrary order.🎤 One-Line Answer for Interview
"I use ORDER BY CreatedDate DESC to sort Leads from newest to oldest and LIMIT 5 to restrict the output — DESC is critical here because the default SOQL sort order is ascending, which would return the oldest records instead of the newest."
Q
Question 06 · 🟠 Intermediate
Retrieve Account names and their associated Opportunities where the Opportunity Amount is greater than $50,000.
✅ Answer
Two approaches — Child to Parent (dot notation) or Parent to Child (sub-query). Both valid — choose based on what data you need as the primary result! 🔗
📌 Approach 1 — Child to Parent (Most Common)
SELECT Account.Name, Name, Amount, StageName, CloseDate
FROM Opportunity
WHERE Amount > 50000
ORDER BY Amount DESC
📌 Approach 2 — Parent to Child (Sub-query)
SELECT Name,
(SELECT Name, Amount, StageName
FROM Opportunities
WHERE Amount > 50000)
FROM Account
WHERE Id IN (SELECT AccountId FROM Opportunity WHERE Amount > 50000)
⚠️
Opportunity is the object name — Opportunities (plural) is the child relationship name used in sub-queries. Wrong name = error.📋 Child to Parent vs Parent to Child
| Type | Syntax | Use When |
|---|---|---|
| Child to Parent | Account.Name (dot notation) | Need Opp data + parent Account name |
| Parent to Child | (SELECT ... FROM Opportunities) | Need Account + all its child Opps |
🎤 One-Line Answer for Interview
"I query from Opportunity and use dot notation Account.Name to traverse up to the parent Account — Child-to-Parent relationship. Amount > 50000 uses no quotes since Amount is a Currency field. The Parent-to-Child sub-query uses the relationship name Opportunities in plural."
Q
Question 07 · 🟠 Intermediate
Find the average amount of all Closed Won Opportunities.
✅ Answer
Use AVG() aggregate function. Since we calculate a single value across all records with no grouping needed, no GROUP BY is required! 📊
📌 Query
SELECT AVG(Amount) avgAmount
FROM Opportunity
WHERE StageName = 'Closed Won'
📋 All SOQL Aggregate Functions
| Function | Purpose | Returns |
|---|---|---|
| COUNT() | Total rows (including nulls) | Integer |
| COUNT(field) | Count of non-null values | Integer |
| SUM(field) | Total sum of numeric field | Decimal |
| AVG(field) | Average of numeric field | Decimal |
| MIN(field) | Minimum value | Same as field type |
| MAX(field) | Maximum value | Same as field type |
💡 All aggregate functions ignore NULL values except
COUNT(). COUNT() counts all rows including those with null fields — a very common interview distinction!💻 In Apex
List<AggregateResult> result = [
SELECT AVG(Amount) avgAmount
FROM Opportunity
WHERE StageName = 'Closed Won'
];
if (!result.isEmpty()) {
Decimal avg = (Decimal) result[0].get('avgAmount');
System.debug('Average: ' + avg);
}
🎤 One-Line Answer for Interview
"I use the AVG() aggregate function on the Amount field with WHERE StageName = 'Closed Won' — since it's a single aggregated value with no grouping, no GROUP BY is needed. In Apex, return type is AggregateResult and I cast the result as Decimal using ar.get('aliasName')."
Q
Question 08 · 🟠 Intermediate
List the Opportunities that are due to close in the next 7 days.
✅ Answer
Use SOQL Date Literal: NEXT_N_DAYS:7. Date Literals are preferred over hardcoded dates — they automatically adjust relative to today! 📅
📌 Query
SELECT Name, Amount, StageName, CloseDate, Account.Name
FROM Opportunity
WHERE CloseDate = NEXT_N_DAYS:7
AND IsClosed = false
ORDER BY CloseDate ASC
📋 Most Important SOQL Date Literals
| Date Literal | Meaning |
|---|---|
| TODAY | Only today's date |
| YESTERDAY / TOMORROW | Yesterday or tomorrow's date |
| THIS_WEEK / THIS_MONTH | Current week (Sun–Sat) or calendar month |
| THIS_FISCAL_YEAR | Current fiscal year as configured in org |
| LAST_N_DAYS:n | Past n days from today (excludes today) |
| NEXT_N_DAYS:n | Next n days from tomorrow (excludes today) |
| LAST_N_MONTHS:n | Past n months |
⚠️
NEXT_N_DAYS:7 does NOT include today. To include today: WHERE CloseDate >= TODAY AND CloseDate <= NEXT_N_DAYS:7💡 Bonus — Overdue open deals:
WHERE CloseDate < TODAY AND IsClosed = false — very useful for sales managers to identify deals that passed their close date!🎤 One-Line Answer for Interview
"I use the SOQL Date Literal NEXT_N_DAYS:7 on CloseDate to dynamically filter Opportunities closing in the next 7 days — Date Literals are preferred over hardcoded dates because they automatically adjust relative to today. I also add IsClosed = false to exclude already closed deals."
Q
Question 09 · 🟠 Intermediate
Retrieve the names and email addresses of all Contacts associated with Opportunities that are in the "Proposal" stage.
✅ Answer
This is a 3-object join — Opportunity + OpportunityContactRole + Contact. Traverse from OCR up to both objects using dot notation simultaneously! 🔗
📌 Approach 1 — With Opportunity Context
SELECT Contact.Name, Contact.Email, Opportunity.Name, Role
FROM OpportunityContactRole
WHERE Opportunity.StageName = 'Proposal/Price Quote'
ORDER BY Contact.Name ASC
📌 Approach 2 — Unique Contacts Only
SELECT Name, Email
FROM Contact
WHERE Id IN (
SELECT ContactId
FROM OpportunityContactRole
WHERE Opportunity.StageName = 'Proposal/Price Quote'
)
ORDER BY Name ASC
⚠️ 'Proposal' alone is NOT a standard Salesforce stage name. The correct standard value is
'Proposal/Price Quote'. Always verify exact picklist values in Setup → Object Manager → Opportunity → StageName.💡 SOQL supports up to 5 levels of parent relationship traversal via dot notation — e.g.
Opportunity.Account.Owner.Profile.Name🎤 One-Line Answer for Interview
"Contact and Opportunity are linked through OpportunityContactRole — I query from OCR and use dot notation to traverse to Contact.Name, Contact.Email, and Opportunity.StageName simultaneously. For unique Contacts without duplicates, I use a sub-query: WHERE Id IN (SELECT ContactId FROM OpportunityContactRole WHERE Opportunity.StageName = 'Proposal/Price Quote')."
Q
Question 10 · 🟠 Intermediate
Find the Account with the maximum number of Opportunities.
✅ Answer
There is no direct MAX(COUNT()) in SOQL. Use the pattern: COUNT → GROUP BY → ORDER BY DESC → LIMIT 1! 🏆
📌 Query
SELECT AccountId, Account.Name, COUNT(Id) totalOpps
FROM Opportunity
GROUP BY AccountId, Account.Name
ORDER BY COUNT(Id) DESC
LIMIT 1
📋 The 4-Step Pattern — Maximum in a Group
| Step | Clause | Purpose |
|---|---|---|
| 1 | COUNT(Id) | Count Opportunities per group |
| 2 | GROUP BY AccountId, Account.Name | Define the grouping |
| 3 | ORDER BY COUNT(Id) DESC | Rank highest count first |
| 4 | LIMIT 1 | Pick only the top result |
⚠️ SOQL does NOT support
MAX(COUNT(Id)) — throws error. Always use the ORDER BY DESC + LIMIT 1 pattern.💡 Handling Ties: If two Accounts have same count, LIMIT 1 returns only one. Use
HAVING COUNT(Id) >= 18 to get all tied Accounts.🎤 One-Line Answer for Interview
"SOQL does not support MAX(COUNT()) directly — so I use COUNT(Id) with GROUP BY AccountId, sort using ORDER BY COUNT(Id) DESC, and pick the top result with LIMIT 1. To handle tie scenarios, I replace LIMIT 1 with a HAVING COUNT(Id) >= n filter."
Q
Question 11 · 🟠 Intermediate
Find the Accounts that have exactly 2 Contacts.
✅ Answer
Use HAVING clause — you CANNOT use
WHERE COUNT(Id) = 2, that throws an error. HAVING is the only way to filter on aggregate functions! 🎯📌 Query
SELECT AccountId, Account.Name, COUNT(Id) totalContacts
FROM Contact
WHERE AccountId != null
GROUP BY AccountId, Account.Name
HAVING COUNT(Id) = 2
ORDER BY Account.Name ASC
📋 WHERE vs HAVING — The Golden Rule
| Clause | Runs When | Filters On | Example |
|---|---|---|---|
| WHERE | Before grouping | Individual record fields | WHERE AccountId != null |
| HAVING | After grouping | Aggregated results only | HAVING COUNT(Id) = 2 |
💡 Both can be used together in the same query —
WHERE filters individual records first, then HAVING filters the aggregated groups after.🎤 One-Line Answer for Interview
"I query from Contact, use COUNT(Id) with GROUP BY AccountId and Account.Name, and apply HAVING COUNT(Id) = 2 to filter only Accounts with exactly 2 Contacts — HAVING is used because WHERE cannot filter on aggregated results, it only works on individual record fields before grouping."
Q
Question 12 · 🟠 Intermediate
Write a SOQL query to fetch the Account record with maximum Opportunities.
✅ Answer
Since ORDER BY and LIMIT are NOT allowed inside SOQL sub-queries, a 2-Step Apex approach is required to fetch the complete Account record! 🔗
📌 Step 1 — Find AccountId with Max Opportunities
SELECT AccountId, COUNT(Id) totalOpps
FROM Opportunity
GROUP BY AccountId
ORDER BY COUNT(Id) DESC
LIMIT 1
📌 Step 2 — Fetch Full Account Record
SELECT Id, Name, Industry, Phone, BillingCity,
Website, Type, AnnualRevenue, Owner.Name
FROM Account
WHERE Id = :topAccountId // bind variable from Step 1
💻 Full Apex Implementation
List<AggregateResult> agg = [
SELECT AccountId, COUNT(Id) totalOpps
FROM Opportunity
GROUP BY AccountId
ORDER BY COUNT(Id) DESC
LIMIT 1
];
if (!agg.isEmpty()) {
Id topId = (Id) agg[0].get('AccountId');
Account acc = [
SELECT Id, Name, Industry, Phone, BillingCity, Owner.Name
FROM Account
WHERE Id = :topId LIMIT 1
];
System.debug(acc.Name + ' — ' + agg[0].get('totalOpps') + ' Opportunities');
}
⚠️ ORDER BY and LIMIT are NOT allowed inside SOQL sub-queries.
WHERE Id IN (SELECT AccountId ... ORDER BY COUNT(Id) DESC LIMIT 1) will throw a compile error. Always use the 2-step Apex approach.🎤 One-Line Answer for Interview
"Since SOQL doesn't allow ORDER BY and LIMIT inside sub-queries, I use a 2-step Apex approach — first an aggregate query on Opportunity with COUNT(Id), GROUP BY AccountId, ORDER BY COUNT(Id) DESC, LIMIT 1 to get the top AccountId, then a second query on Account using that Id to fetch the complete record."
Q
Question 13 · 🟢 Basic
Write a SOQL query to fetch the latest 10 records of the Account.
✅ Answer
Use ORDER BY CreatedDate DESC + LIMIT 10. For pagination beyond 2000 records, switch from OFFSET to Keyset Pagination! 📋
📌 Query — Latest 10 by Creation Date
SELECT Id, Name, Industry, Phone, BillingCity,
BillingCountry, Type, CreatedDate,
CreatedBy.Name, Owner.Name
FROM Account
ORDER BY CreatedDate DESC
LIMIT 10
📌 Bonus — Pagination using OFFSET
// Page 1 → Records 1–10
SELECT Id, Name, CreatedDate FROM Account
ORDER BY CreatedDate DESC LIMIT 10 OFFSET 0
// Page 2 → Records 11–20
SELECT Id, Name, CreatedDate FROM Account
ORDER BY CreatedDate DESC LIMIT 10 OFFSET 10
📋 Standard Audit Fields — Quick Reference
| Field | Purpose | Changeable? |
|---|---|---|
| CreatedDate | When record was first created | ❌ Never |
| CreatedById | User who created the record | ❌ Never |
| LastModifiedDate | When record was last edited | ✅ Auto-updated |
| LastModifiedById | User who last edited the record | ✅ Auto-updated |
| SystemModstamp | Last modified by system or user (includes automation) | ✅ Auto-updated |
⚠️ OFFSET has a hard limit of 2000 records. For datasets beyond 2000, use Keyset Pagination:
WHERE CreatedDate < :lastCreatedDate ORDER BY CreatedDate DESC LIMIT 10🎤 One-Line Answer for Interview
"I use ORDER BY CreatedDate DESC to sort Account records newest to oldest and LIMIT 10 to restrict output — DESC is critical because default SOQL sort is ascending. For pagination beyond 2000 records, OFFSET has a hard limit so I switch to keyset pagination using the last CreatedDate value as a cursor."
📚 Keep Preparing
New interview questions every week 🚀
Follow for fresh Salesforce Q&A, free courses, and real interview experiences — straight from the trenches.
👥 Follow on LinkedIn