Top Salesforce SOQL Interview Questions & Answers — With Apex Code & Real Examples (2025)

📅  Soql
SOQL Interview Questions — Salesforce Interview Prep
🗄️ SOQL

Salesforce SOQL Interview Questions —
Queries, Aggregates & Relationships Explained

BEGINNER TO INTERMEDIATE
Q
Retrieve all Account names and their corresponding Industry from Salesforce.
📌 Layered Concept

Basic SELECT statement in SOQL. SOQL (Salesforce Object Query Language) is used to query records from the Salesforce database — similar to SQL, but built specifically for Salesforce objects. Unlike SQL, SOQL does not support SELECT * — you must always explicitly specify field names.

Query
SELECT Name, Industry
FROM   Account
ORDER BY Name ASC
🧠
Breaking It Down
ClausePurpose
SELECT Name, IndustryFetch only these two fields from the Account object
FROM AccountTarget the standard Account sObject
ORDER BY Name ASCSort results alphabetically by Account name
⚠️ No SELECT * in SOQL. Unlike SQL, writing SELECT * FROM Account will throw an error in Salesforce. Always mention 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-LINER 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
Find the total number of Opportunities for each Account.
📌 Aggregate Functions + GROUP BY

To count Opportunities per Account, we use COUNT(Id) to count records and GROUP BY to group results by Account. 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
ClausePurpose
COUNT(Id)Counts total Opportunities for each Account
GROUP BY AccountId, Account.NameRequired when using any aggregate function
ORDER BY COUNT(Id) DESCShows 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-LINER 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
Retrieve the names of Contacts associated with an Opportunity named "XYZ Opportunity."
📌 Junction Object — OpportunityContactRole

Contact and Opportunity are not directly related in Salesforce. They are connected through the standard junction object OpportunityContactRole.

🔗 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 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, querying the Contact object directly.
🏆 ONE-LINER 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
List all Closed Won Opportunities along with their amounts.
📌 WHERE Clause — Picklist Field Filtering

Every Opportunity has a standard field StageName which tracks the sales stage. Closed Won is a standard picklist value meaning the deal is successfully closed. Picklist values in SOQL are 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
StageNameIsClosedIsWonMeaning
ProspectingfalsefalseEarly stage lead
Proposal/Price QuotefalsefalseQuote shared
Closed WontruetrueDeal won ✅
Closed LosttruefalseDeal lost ❌
💡 IsClosed = true returns BOTH Closed Won AND Closed Lost. Use StageName = 'Closed Won' specifically for only won deals.
🏆 ONE-LINER 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
Identify the five most recently created Leads.
📌 ORDER BY + LIMIT + Audit Fields

Every Salesforce record has a system-generated audit field CreatedDate — auto-populated when the record is first created and never changes. To get the newest records, sort by CreatedDate DESC and restrict to 5 using LIMIT.

Query
SELECT Name, Email, Phone, Company, LeadSource, Status, CreatedDate
FROM   Lead
ORDER BY CreatedDate DESC
LIMIT  5
⚠️ Forgetting DESC returns the OLDEST 5 leads — ASC is the default. Always pair LIMIT with ORDER BY for predictable results.
🧠
LIMIT — Key Rules
ScenarioBehaviour
LIMIT 1Exactly 1 record — used when you expect a unique result
LIMIT 200Default max records shown in Developer Console
LIMIT 50000Maximum 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.
🏆 ONE-LINER 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
Retrieve Account names and their associated Opportunities where the Opportunity Amount is greater than $50,000.
📌 Relationship Queries — Child to Parent & Parent to Child

Opportunity has a standard lookup to Account. Two approaches — query from Opportunity and traverse up to Account via dot notation (Child to Parent), or write a sub-query from Account down to Opportunities (Parent to Child).

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
TypeSyntaxUse When
Child to ParentAccount.Name (dot notation)Need Opp data + parent Account name
Parent to Child(SELECT ... FROM Opportunities)Need Account + all its child Opps
🏆 ONE-LINER 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
Find the average amount of all Closed Won Opportunities.
📌 AVG() Aggregate Function

AVG() calculates the mathematical average of a numeric or currency field across all matching records. Since we calculate a single aggregated value across all records (no grouping needed), no GROUP BY is required here.

Query
SELECT AVG(Amount) avgAmount
FROM   Opportunity
WHERE  StageName = 'Closed Won'
🧠
All SOQL Aggregate Functions
FunctionPurposeReturns
COUNT()Total rows (including nulls)Integer
COUNT(field)Count of non-null valuesInteger
SUM(field)Total sum of numeric fieldDecimal
AVG(field)Average of numeric fieldDecimal
MIN(field)Minimum valueSame as field type
MAX(field)Maximum valueSame 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-LINER 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
List the Opportunities that are due to close in the next 7 days.
📌 SOQL Date Literals

SOQL provides built-in Date Literals — special keywords that represent dynamic date ranges without hardcoding any specific date. NEXT_N_DAYS:7 automatically calculates the next 7 days relative to today every time the query runs.

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 LiteralMeaning
TODAYOnly today's date
YESTERDAY / TOMORROWYesterday or tomorrow's date
THIS_WEEK / THIS_MONTHCurrent week (Sun–Sat) or calendar month
THIS_FISCAL_YEARCurrent fiscal year as configured in org
LAST_N_DAYS:nPast n days from today (excludes today)
NEXT_N_DAYS:nNext n days from tomorrow (excludes today)
LAST_N_MONTHS:nPast 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-LINER 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
Retrieve the names and email addresses of all Contacts associated with Opportunities that are in the "Proposal" stage.
📌 Multi-Level Relationship Query — 3 Object Join

This is a 3-object join — Opportunity, OpportunityContactRole, and Contact. We traverse from OCR up to both Opportunity (for stage filter) and Contact (for name/email) 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 (no duplicates)
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-LINER 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
Find the Account with the maximum number of Opportunities.
📌 COUNT + GROUP BY + ORDER BY DESC + LIMIT 1

There is no direct MAX(COUNT()) in SOQL. The standard pattern is: count Opportunities per Account using COUNT(Id), group by Account, sort by count descending, and pick only the top 1 using 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
StepClausePurpose
1COUNT(Id)Count Opportunities per group
2GROUP BY AccountId, Account.NameDefine the grouping
3ORDER BY COUNT(Id) DESCRank highest count first
4LIMIT 1Pick 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-LINER 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
Find the Accounts that have exactly 2 Contacts.
📌 HAVING Clause — Filter on Aggregated Results

WHERE filters individual records before grouping. HAVING filters aggregated results after grouping. 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
ClauseRuns WhenFilters OnExample
WHEREBefore groupingIndividual record fieldsWHERE AccountId != null
HAVINGAfter groupingAggregated results onlyHAVING 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-LINER ANSWER FOR INTERVIEW
"I query from Contact, use COUNT(Id) with GROUP BY AccountId, 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
Write a SOQL query to fetch the Account record with maximum Opportunities.
📌 2-Step Query Pattern in Apex

This extends Q10 — here the interviewer wants the complete Account record with all fields. Since SOQL does not allow ORDER BY and LIMIT inside sub-queries, a 2-step Apex approach is required.

1️⃣
Step 1 — Find AccountId with Max Opportunities
SELECT AccountId, COUNT(Id) totalOpps
FROM   Opportunity
GROUP BY AccountId
ORDER BY COUNT(Id) DESC
LIMIT  1
2️⃣
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-LINER 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
Write a SOQL query to fetch the latest 10 records of the Account.
📌 ORDER BY + LIMIT + Standard Audit Fields + OFFSET Pagination

Every Salesforce object has 4 system-generated audit fields automatically populated by Salesforce — CreatedDate, CreatedById, LastModifiedDate, LastModifiedById. To get the latest 10 Accounts, sort by CreatedDate DESC and limit to 10.

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
FieldPurposeChangeable?
CreatedDateWhen record was first createdNever
CreatedByIdUser who created the recordNever
LastModifiedDateWhen record was last editedAuto-updated
LastModifiedByIdUser who last edited the recordAuto-updated
SystemModstampLast 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-LINER 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."