Top Salesforce SOQL Interview Questions & Answers — With Apex Code & Real Examples (2025)
Salesforce SOQL Interview Questions —
Queries, Aggregates & Relationships Explained
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.
SELECT Name, Industry FROM Account ORDER BY Name ASC
| 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 |
SELECT * FROM Account will throw an error in Salesforce. Always mention field API names explicitly.List<Account> accounts = [ SELECT Name, Industry FROM Account ORDER BY Name ASC ]; for (Account acc : accounts) { System.debug(acc.Name + ' | ' + acc.Industry); }
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.
SELECT AccountId, Account.Name, COUNT(Id) total FROM Opportunity GROUP BY AccountId, Account.Name ORDER BY COUNT(Id) DESC
| 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 |
HAVING (not WHERE) to filter on aggregated results — e.g. HAVING COUNT(Id) > 5.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')); }
Contact and Opportunity are not directly related in Salesforce. They are connected through the standard junction object OpportunityContactRole.
SELECT Contact.Name, Contact.Email, Role FROM OpportunityContactRole WHERE Opportunity.Name = 'XYZ Opportunity'
SELECT Name, Email FROM Contact WHERE Id IN ( SELECT ContactId FROM OpportunityContactRole WHERE Opportunity.Name = 'XYZ Opportunity' )
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.
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'.| 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.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.
SELECT Name, Email, Phone, Company, LeadSource, Status, CreatedDate FROM Lead ORDER BY CreatedDate DESC LIMIT 5
DESC returns the OLDEST 5 leads — ASC is the default. Always pair LIMIT with ORDER BY for predictable results.| 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 is set ONCE when record is created — never changes. LastModifiedDate updates every time the record is edited.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).
SELECT Account.Name, Name, Amount, StageName, CloseDate FROM Opportunity WHERE Amount > 50000 ORDER BY Amount DESC
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.| 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 |
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.
SELECT AVG(Amount) avgAmount FROM Opportunity WHERE StageName = 'Closed Won'
| 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 |
COUNT(). COUNT() counts all rows including those with null fields — a very common interview distinction!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); }
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.
SELECT Name, Amount, StageName, CloseDate, Account.Name FROM Opportunity WHERE CloseDate = NEXT_N_DAYS:7 AND IsClosed = false ORDER BY CloseDate ASC
| 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:7WHERE CloseDate < TODAY AND IsClosed = false — very useful for sales managers to identify deals that passed their close date!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.
SELECT Contact.Name, Contact.Email, Opportunity.Name, Role FROM OpportunityContactRole WHERE Opportunity.StageName = 'Proposal/Price Quote' ORDER BY Contact.Name ASC
SELECT Name, Email FROM Contact WHERE Id IN ( SELECT ContactId FROM OpportunityContactRole WHERE Opportunity.StageName = 'Proposal/Price Quote' ) ORDER BY Name ASC
'Proposal/Price Quote'. Always verify exact picklist values in Setup → Object Manager → Opportunity → StageName.Opportunity.Account.Owner.Profile.NameThere 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.
SELECT AccountId, Account.Name, COUNT(Id) totalOpps FROM Opportunity GROUP BY AccountId, Account.Name ORDER BY COUNT(Id) DESC LIMIT 1
| 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 |
MAX(COUNT(Id)) — throws error. Always use the ORDER BY DESC + LIMIT 1 pattern.HAVING COUNT(Id) >= 18 to get all tied Accounts.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.
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
| 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 |
WHERE filters individual records first, then HAVING filters the aggregated groups after.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.
SELECT AccountId, COUNT(Id) totalOpps FROM Opportunity GROUP BY AccountId ORDER BY COUNT(Id) DESC LIMIT 1
SELECT Id, Name, Industry, Phone, BillingCity, Website, Type, AnnualRevenue, Owner.Name FROM Account WHERE Id = :topAccountId // bind variable from Step 1
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'); }
WHERE Id IN (SELECT AccountId ... ORDER BY COUNT(Id) DESC LIMIT 1) will throw a compile error. Always use the 2-step Apex approach.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.
SELECT Id, Name, Industry, Phone, BillingCity, BillingCountry, Type, CreatedDate, CreatedBy.Name, Owner.Name FROM Account ORDER BY CreatedDate DESC LIMIT 10
-- 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
| 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 |
WHERE CreatedDate < :lastCreatedDate ORDER BY CreatedDate DESC LIMIT 10