SOQL & SOSL

Medium & Hard SOQL & SOSL Practice Problems

68 free Salesforce Apex challenges · Full problem statements, best-practice notes, and step-by-step hints · Solve live on ApexArena

This guide walks through 68 aggregate queries, subqueries, multi-level relationship traversal, and SOSL search challenges. Each entry below includes the full problem statement, the governor-limit and best-practice constraints it's testing, and a numbered approach to solving it — then you can open the same problem in ApexArena's browser-based Apex editor and get instant pass/fail feedback against real test cases.

On this page
  1. 1. Nth Highest Salary Finder
  2. 2. Dynamic SOQL Builder
  3. 3. Parent-to-Child Relationship Query
  4. 4. Semi-Join: Contacts at Accounts with Won Deals
  5. 5. Anti-Join: Accounts with No Open Cases
  6. 6. GROUP BY + HAVING: Accounts with More than 2 Contacts
  7. 7. Total Opportunities Per Account
  8. 8. Contacts Associated with a Specific Opportunity
  9. 9. Accounts with Opportunities Greater Than $50,000
  10. 10. Contacts on Opportunities in the Proposal Stage
  11. 11. Account with the Maximum Number of Opportunities
  12. 12. Accounts That Have Both Opportunities and Cases
  13. 13. Opportunities Not Updated in the Last 30 Days
  14. 14. Contacts Whose Mailing State Differs from Their Account's Billing State
  15. 15. Account Names with Total Amount of All Associated Opportunities
  16. 16. Contacts Related to Opportunities with Above-Average Amount
  17. 17. Top 3 Opportunities with the Highest Amount for Each Account
  18. 18. Opportunities with Approval Status Set to "Pending"
  19. 19. Accounts Not Modified in the Last 60 Days
  20. 20. Closed Won Opportunities with a Close Date in the Last Quarter
  21. 21. Account Names with the Total Number of Associated Contacts
  22. 22. High-Priority Opportunities with Amount Greater Than $50,000
  23. 23. Accounts That Have at Least One Prospecting Opportunity
  24. 24. SOQL Child-to-Parent: Opportunity with Account Owner (2-Level)
  25. 25. SOQL Child-to-Parent: Filter Contacts by Account Type
  26. 26. SOQL Child-to-Parent: Case with Contact and Account Names
  27. 27. SOQL Child-to-Parent: Filter Opportunities by Account Revenue
  28. 28. SOQL Child-to-Parent: Contact Directory with Multiple Account Fields
  29. 29. SOQL Child-to-Parent: Contact with Account Owner (2-Level)
  30. 30. SOQL Parent-to-Child: Account with Open Cases
  31. 31. SOQL Parent-to-Child: Account with Minimal Contact Subquery
  32. 32. SOQL Parent-to-Child: Account with Contacts Filtered by Email
  33. 33. SOQL Parent-to-Child: Account with Opportunities Ordered by Close Date
  34. 34. SOQL Parent-to-Child: Account with Contacts Sorted NULLS LAST
  35. 35. SOQL Parent-to-Child: Account with Two Subqueries (Contacts + Opportunities)
  36. 36. SOQL Parent-to-Child: Account with Cases and Case Comments
  37. 37. SOQL Combined: Child-to-Parent + Parent-to-Child
  38. 38. SOQL: Aggregate — Accounts with 5 or More Contacts
  39. 39. SOQL: Semi-Join and Anti-Join Subqueries
  40. 40. SOQL: Dynamic SOQL with String.escapeSingleQuotes
  41. 41. SOQL: Aggregate with CALENDAR_MONTH and FISCAL_YEAR Date Functions
  42. 42. SOQL: Sum of All Opportunity Amounts
  43. 43. SOQL: Average Amount of All Opportunities
  44. 44. SOQL: Maximum Opportunity Amount
  45. 45. SOQL: Minimum Opportunity Amount
  46. 46. SOQL: Count Opportunities Grouped by Stage
  47. 47. SOQL: Filter Groups With HAVING Clause
  48. 48. SOQL: Child-to-Parent Relationship — Contact to Account Name
  49. 49. SOQL: Child-to-Parent Relationship — Opportunity to Account Name
  50. 50. SOQL: Multi-Level Child-to-Parent — Opportunity Owner Profile
  51. 51. SOQL: Parent-to-Child Subquery — Account With Contacts
  52. 52. SOQL: Parent-to-Child Subquery — Account With Opportunities
  53. 53. SOQL: Parent-to-Child Subquery With WHERE Filter
  54. 54. SOQL: Semi-Join — Accounts That Have Opportunities
  55. 55. SOQL: Anti-Join — Accounts Without Any Opportunities
  56. 56. SOQL: Total Opportunity Amount Grouped by Stage
  57. 57. SOQL: Dynamic SOQL Using Database.query()
  58. 58. SOQL: Count Distinct Opportunity Owners per Stage
  59. 59. SOQL: Multi-Field GROUP BY With HAVING — High-Revenue Stage/Type Combinations
  60. 60. SOQL: Leads Without a Matching Converted Contact by Email
  61. 61. SOQL: Case Count by Priority for a Specific Account
  62. 62. SOQL: Contacts With Their Account's Annual Revenue
  63. 63. SOQL: Each Account's Highest-Value Open Opportunity
  64. 64. SOQL: Two-Level Child-to-Parent — Contact's Account Owner Name
  65. 65. SOQL: Opportunities Missing a Next Step
  66. 66. SOQL: Accounts With More Than One Open Opportunity (GROUP BY + HAVING)
  67. 67. SOQL: Accounts With Both Open Opportunities and Contacts
  68. 68. Scenario: Multi-Currency-Aware Opportunity Revenue Rollup
Hard SOQL

1. Nth Highest Salary Finder

Problem #3 · Salesforce Apex Coding Challenge

Problem Statement

Write a SOQL query to find the Nth highest Salary__c from a custom object Employee__c. Return the full employee record at that salary rank.

Handle ties by counting distinct salary values.

Example

Salaries: [90, 85, 85, 70, 60]
N = 2 → employees with salary 85
N = 3 → employees with salary 70
Approach
  • 1The aggregate query returns N distinct salaries in DESC order — the last element is the Nth highest.
  • 2Then query: SELECT Id, Name, Salary__c FROM Employee__c WHERE Salary__c = :nthSalary
  • 3Edge case: if fewer than N distinct salaries exist, return an empty list.
Hard SOQLGovernor

2. Dynamic SOQL Builder

Problem #20 · Salesforce Apex Coding Challenge

Problem Statement

Build a reusable utility method that constructs and executes a dynamic SOQL query from parameters, following all Salesforce security best practices.

Method Signature

public static List<SObject> buildAndRun(
    String objectType,
    List<String> fields,
    String whereClause,
    Integer limitCount
)

Security Requirements

  • Use String.escapeSingleQuotes() on the where clause to prevent SOQL injection
  • Always include a LIMIT clause (default to 200 if null)
  • Use String.join() to concatenate field names
  • Call Database.query() to execute
Approach
  • 1Build the query string: 'SELECT ' + String.join(fields, ', ') + ' FROM ' + objectType
  • 2Always escape: WHERE ' + String.escapeSingleQuotes(whereClause)
  • 3Append LIMIT: ' LIMIT ' + (limitCount != null ? limitCount : 200)
  • 4Execute with: return Database.query(query);
Medium SOQL

3. Parent-to-Child Relationship Query

Problem #82 · Salesforce Apex Coding Challenge

Problem Statement

Write a static method that returns all Account records along with their related Contact records in a single SOQL query using a child relationship subquery in the SELECT clause.

Method Signature

public static List<Account> getAccountsWithContacts()

Key Concept — Child Subquery

A child relationship subquery sits inside the outer SELECT clause in parentheses. You use the relationship name (the plural form of the child object, for a standard-object relationship) rather than the object name itself.

SELECT Id, Name, (SELECT Id, SomeField FROM ChildRelationshipName)
FROM ParentObject

Constraints

  • Use a single SOQL query — no loops, no separate Contact query
  • Include at least Id and LastName in the subquery SELECT
  • Order results by Account.Name ASC
  • No ORDER BY or LIMIT inside the child subquery (not allowed)
Approach
  • 1The child relationship name for Contact under Account is Contacts (plural, no __r suffix for standard objects).
  • 2A child subquery goes inside the outer SELECT clause, wrapped in parentheses, and uses the relationship name instead of the object name.
  • 3Access child records in Apex: for (Contact c : acc.Contacts) { ... }
Medium SOQL

4. Semi-Join: Contacts at Accounts with Won Deals

Problem #83 · Salesforce Apex Coding Challenge

Problem Statement

Write a method that returns all Contact records whose parent Account has at least one Closed Won Opportunity — using a SOQL semi-join (IN with a subquery).

Method Signature

public static List<Contact> getContactsAtWinningAccounts()

Key Concept — Semi-Join

A semi-join filters the outer query by checking whether a relationship field appears in the result of a subquery:

WHERE SomeField IN (SELECT ParentField FROM ChildObject WHERE ...)

The subquery must select a single field that matches the type of the outer WHERE field.

Constraints

  • Use a single SOQL statement — no Apex loops to compare records
  • The subquery must query Opportunity and filter on StageName
  • Return at least Id, FirstName, LastName, AccountId
Approach
  • 1A semi-join filters the outer query with WHERE field IN (subquery) — the subquery runs against the related object and returns the matching parent Ids.
  • 2The subquery field must be an Id or relationship field — it must match the type of the outer WHERE field.
  • 3You cannot use ORDER BY or LIMIT inside a semi-join subquery.
Medium SOQL

5. Anti-Join: Accounts with No Open Cases

Problem #84 · Salesforce Apex Coding Challenge

Problem Statement

Write a method that returns all Account records that have no open Cases — using a SOQL anti-join (NOT IN with a subquery).

Method Signature

public static List<Account> getAccountsWithNoOpenCases()

Key Concept — Anti-Join

An anti-join excludes records whose Id appears in a subquery result:

WHERE Id NOT IN (SELECT ParentField FROM ChildObject WHERE ...)

Constraints

  • Use NOT IN with a subquery — not a loop-based approach
  • Subquery must filter for open (non-Closed) Cases
  • Return at least Id and Name
  • Order results by Name ASC
Approach
  • 1An anti-join excludes records with WHERE field NOT IN (subquery) — the subquery returns the Ids that SHOULD be excluded from the outer result.
  • 2The subquery field must be an Id-type field that matches the outer WHERE field (Id).
  • 3A lookup field can be null on some child records — to be safe, also filter those out inside the subquery.
Hard SOQLGovernor

6. GROUP BY + HAVING: Accounts with More than 2 Contacts

Problem #85 · Salesforce Apex Coding Challenge

Problem Statement

Write a method that uses a SOQL aggregate query to find all Account IDs that have more than 2 related Contacts.

Method Signature

public static List<AggregateResult> getHighContactAccounts()

Key Concept — HAVING

HAVING filters after aggregation — it is the equivalent of WHERE for aggregate results. You cannot use WHERE to filter on a COUNT() result.

SELECT GroupField, COUNT(Id) alias
FROM ChildObject
GROUP BY GroupField
HAVING COUNT(Id) > someThreshold

Constraints

  • Use GROUP BY AccountId — do not loop over contacts in Apex
  • Use HAVING COUNT(Id) > 2 to filter at the database level
  • Order by COUNT(Id) DESC to show most-contacted accounts first
  • Access results with: (Integer) result.get('cnt')
Approach
  • 1HAVING filters after GROUP BY — use it to filter on aggregate values like COUNT(Id).
  • 2Alias the count for easy access, e.g. COUNT(Id) cnt — then use result.get('cnt') in Apex.
  • 3The HAVING expression must reference the same aggregate function used in SELECT.
Medium SOQLAggregate

7. Total Opportunities Per Account

Problem #108 · Salesforce Apex Coding Challenge

Problem Statement

Write a SOQL aggregate query to find the total number of Opportunities for each Account, returning the highest-count accounts first.

Requirements

  • SELECT AccountId, COUNT(Id) oppCount
  • FROM Opportunity
  • GROUP BY AccountId
  • ORDER BY COUNT(Id) DESC
  • Return type: List<AggregateResult>

Access Results

for (AggregateResult ar : results) {
    Id accId     = (Id)      ar.get('AccountId');
    Integer cnt  = (Integer) ar.get('oppCount');
}
Approach
  • 1COUNT(Id) oppCount gives each group a named alias.
  • 2GROUP BY AccountId splits results into one row per account.
  • 3ORDER BY COUNT(Id) DESC puts the busiest accounts first.
Hard SOQLSubquery

8. Contacts Associated with a Specific Opportunity

Problem #109 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve the names and emails of all Contacts whose Account has an Opportunity matching a given opportunity name.

Use a semi-join to find relevant AccountIds from the Opportunity object, then return Contacts for those accounts.

Requirements

  • Return each matching Contact's Id, Name, and Email.
  • Use a semi-join against Opportunity to find the qualifying Accounts — no Apex loops.
  • Method parameter: String opportunityName
Approach
  • 1Contacts are not directly linked to Opportunities — use Account as the bridge.
  • 2A semi-join (WHERE field IN (subquery)) lets you filter Contacts by a condition on their related Opportunity without an Apex loop.
  • 3Bind the parameter with :opportunityName inside the subquery.
Medium SOQLSubquery

9. Accounts with Opportunities Greater Than $50,000

Problem #112 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve Account names that have at least one associated Opportunity with an Amount greater than $50,000.

Use a semi-join to avoid duplicate Account rows.

Requirements

  • Return each qualifying Account's Id, Name, and Industry.
  • Use a semi-join against Opportunity to find Accounts with a large enough deal — no Apex loops.
  • Sort the result alphabetically by Account Name.
Approach
  • 1A semi-join (WHERE field IN (subquery)) lets you filter Accounts by a condition on their related Opportunities without an Apex loop.
  • 2Outer object is Account; inner object is Opportunity.
  • 3Amount values in SOQL are numeric — no $ symbol.
Medium SOQLSubquery

10. Contacts on Opportunities in the Proposal Stage

Problem #115 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve the names and email addresses of all Contacts whose Account has at least one Opportunity in the "Proposal/Price Quote" stage.

Approach

Use a semi-join: filter Contacts by AccountIds that have a Proposal-stage Opportunity.

Requirements

  • Return each qualifying Contact's Id, Name, and Email.
  • Use a semi-join against Opportunity to find the qualifying Accounts — no Apex loops.
Approach
  • 1Contact is not directly linked to Opportunity — use Account as the bridge.
  • 2A semi-join on AccountId, filtered to Accounts with a matching Opportunity stage, avoids an Apex loop.
  • 3Full stage name is 'Proposal/Price Quote' — include the slash.
Hard SOQLAggregate

11. Account with the Maximum Number of Opportunities

Problem #116 · Salesforce Apex Coding Challenge

Problem Statement

Find and return the AccountId of the Account that has the highest number of associated Opportunities.

Requirements

  • SELECT AccountId, COUNT(Id) cnt
  • GROUP BY AccountId → ORDER BY COUNT(Id) DESC → LIMIT 1
  • Return the AccountId from the first (and only) result row
  • Return null if no Opportunities exist
Approach
  • 1GROUP BY AccountId + ORDER BY COUNT(Id) DESC + LIMIT 1 gives the single top account.
  • 2Access with: (Id) results[0].get('AccountId').
  • 3Return null if results is empty.
Hard SOQLSubquery

12. Accounts That Have Both Opportunities and Cases

Problem #118 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve all Accounts that have both at least one Opportunity and at least one Case, using two semi-joins combined with AND.

Requirements

  • Include only Accounts that have at least one related Opportunity.
  • Also require at least one related Case on the same Account.
  • Single SOQL statement — no Apex loops.
Approach
  • 1Chain two semi-joins together with AND — one per related object.
  • 2First semi-join filters by Opportunity ownership.
  • 3Second semi-join filters by Case ownership.
Medium SOQLDate Literals

13. Opportunities Not Updated in the Last 30 Days

Problem #119 · Salesforce Apex Coding Challenge

Problem Statement

Identify Opportunities that have not been modified in the last 30 days.

Key Concept

LastModifiedDate < LAST_N_DAYS:30 means the record was modified more than 30 days ago — i.e., it sits outside (before) the rolling window.

Requirements

  • WHERE LastModifiedDate < LAST_N_DAYS:30
  • SELECT Id, Name, StageName, LastModifiedDate
  • ORDER BY LastModifiedDate ASC (oldest first)
Approach
  • 1LAST_N_DAYS:30 is a rolling 30-day window ending now.
  • 2Use < to find records whose last modification is BEFORE (older than) that window.
  • 3ORDER BY LastModifiedDate ASC surfaces the most stale records first.
Hard SOQLRelationship Query

14. Contacts Whose Mailing State Differs from Their Account's Billing State

Problem #122 · Salesforce Apex Coding Challenge

Problem Statement

Find Contacts whose MailingState is not equal to their parent Account's BillingState — these Contacts are located in a different state than their company's billing address.

Key Concept — SOQL Cannot Compare Two Fields Directly

Unlike standard SQL, a SOQL WHERE clause can only compare a field against a literal, bind variable, or subquery — never against another field (WHERE MailingState != Account.BillingState is not valid SOQL and will not compile). The correct pattern is to query the candidate records with only null guards in the WHERE clause, then perform the actual field-to-field comparison in Apex.

Requirements

  • Query Contacts selecting Id, Name, MailingState and the related Account.BillingState
  • Guard the query against nulls on both MailingState and Account.BillingState so blank addresses don't produce false mismatches
  • Loop over the query results in Apex and compare c.MailingState != c.Account.BillingState, collecting only the mismatches to return
Approach
  • 1Traverse the Account lookup with dot-notation: Account.BillingState.
  • 2SOQL's WHERE clause cannot compare two fields directly — WHERE MailingState != Account.BillingState is invalid syntax.
  • 3Query with only null guards in WHERE, then compare c.MailingState != c.Account.BillingState inside an Apex loop.
Medium SOQLAggregate

15. Account Names with Total Amount of All Associated Opportunities

Problem #124 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve each Account's total Opportunity Amount — the sum of all Opportunity Amounts grouped by Account, highest total first.

Requirements

  • SELECT AccountId, SUM(Amount) totalAmount
  • FROM Opportunity
  • GROUP BY AccountId
  • ORDER BY SUM(Amount) DESC

Access

Decimal total = (Decimal) ar.get('totalAmount');
Approach
  • 1SUM(Amount) totalAmount sums all opportunity amounts per account group.
  • 2GROUP BY AccountId creates one result row per account.
  • 3ORDER BY SUM(Amount) DESC puts the account with the most pipeline value first.
Hard SOQLAggregateCollections

16. Contacts Related to Opportunities with Above-Average Amount

Problem #126 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve the names and emails of all Contacts whose Account has at least one Opportunity with an Amount greater than the average Opportunity Amount.

3-Step Approach

  1. Compute the average Opportunity Amount using an aggregate query.
  2. Collect the AccountIds of Opportunities whose Amount exceeds that average.
  3. Fetch the Name and Email of Contacts belonging to those Accounts.

Governor Rules

  • No SOQL inside loops — collect IDs first, then query
  • Maximum 3 SOQL queries total
Approach
  • 1Step 1 — use an AVG aggregate over Opportunity Amount.
  • 2Step 2 — bind the computed average into a WHERE Amount > :avgAmt filter.
  • 3Step 3 — collect matching AccountIds into a Set, then query Contact filtered by that set.
  • 4Never put SOQL inside a loop — collect all IDs first.
Hard SOQLCollections

17. Top 3 Opportunities with the Highest Amount for Each Account

Problem #127 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve the top 3 Opportunities by Amount for each Account, returned as a Map<Id, List<Opportunity>> keyed by AccountId.

Why Apex?

SOQL does not support LIMIT n PER GROUP, so post-query Apex logic is needed to cap each account's list at 3.

Requirements

  • One SOQL query: ORDER BY Amount DESC
  • Iterate and stop adding once a group reaches 3 entries
  • Return Map<Id, List<Opportunity>>
Approach
  • 1ORDER BY Amount DESC means the first opportunity you encounter per account is the highest.
  • 2containsKey(opp.AccountId) checks whether the account already has a list entry.
  • 3.size() < 3 before adding ensures each account never exceeds 3 records.
Medium SOQLCustom Fields

18. Opportunities with Approval Status Set to "Pending"

Problem #128 · Salesforce Apex Coding Challenge

Problem Statement

Identify Opportunities where the custom field Approval_Status__c is set to "Pending".

Custom Field Convention

All custom fields in Salesforce end with __c. Reference them exactly as defined — casing and underscores matter.

Requirements

  • Filter Opportunities to those whose Approval_Status__c equals "Pending"
  • Include Approval_Status__c in the selected fields
  • Sort the results by Amount, highest first
Approach
  • 1Custom fields always use the __c suffix: Approval_Status__c.
  • 2Filter on the Approval_Status__c field equaling the exact text "Pending".
  • 3Include Approval_Status__c among the selected fields so callers can verify the value.
Medium SOQLDate Literals

19. Accounts Not Modified in the Last 60 Days

Problem #129 · Salesforce Apex Coding Challenge

Problem Statement

Find all Accounts that have not been modified in the last 60 days. These are candidates for re-engagement or data cleanup.

Key Concept

LastModifiedDate < LAST_N_DAYS:60 returns records whose last modification is before the 60-day rolling window (older than 60 days).

Requirements

  • WHERE LastModifiedDate < LAST_N_DAYS:60
  • ORDER BY LastModifiedDate ASC (oldest first)
Approach
  • 1LAST_N_DAYS:60 is a rolling window. Use < to find records OLDER than that window.
  • 2Contrast: > finds records modified WITHIN the last 60 days.
  • 3ORDER BY LastModifiedDate ASC surfaces the most stale accounts at the top.
Medium SOQLDate Literals

20. Closed Won Opportunities with a Close Date in the Last Quarter

Problem #130 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve all Closed Won Opportunities whose CloseDate falls within the last calendar quarter.

Key Concept

LAST_QUARTER covers the complete previous fiscal quarter (Q1, Q2, Q3, or Q4) relative to today.

Requirements

  • WHERE StageName = 'Closed Won' AND CloseDate = LAST_QUARTER
  • ORDER BY Amount DESC
Approach
  • 1LAST_QUARTER covers the entire previous calendar quarter automatically.
  • 2Combine: StageName = 'Closed Won' AND CloseDate = LAST_QUARTER.
  • 3No date arithmetic needed — LAST_QUARTER is a built-in date literal.
Medium SOQLAggregate

21. Account Names with the Total Number of Associated Contacts

Problem #131 · Salesforce Apex Coding Challenge

Problem Statement

Write a SOQL aggregate query that returns each Account's total Contact count, ordered from most contacts to fewest.

Requirements

  • SELECT AccountId, COUNT(Id) contactCount
  • FROM Contact — not Account
  • GROUP BY AccountId
  • ORDER BY COUNT(Id) DESC
Approach
  • 1Query FROM Contact — the Contact holds the AccountId foreign key.
  • 2COUNT(Id) contactCount counts how many contacts each account has.
  • 3ORDER BY COUNT(Id) DESC ranks accounts by their contact volume.
Medium SOQLCustom Fields

22. High-Priority Opportunities with Amount Greater Than $50,000

Problem #133 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve Opportunities where the custom field Priority__c is "High" and the Amount exceeds $50,000.

Requirements

  • Filter to Opportunities where Priority__c is "High" and Amount exceeds 50,000
  • Include Priority__c among the selected fields
  • Sort the results by Amount, highest first
Approach
  • 1Custom fields use __c suffix: Priority__c.
  • 2Combine both filters: Priority__c must equal 'High' and Amount must exceed 50000.
  • 3Numeric SOQL comparisons use no $ symbol — just the raw number.
Medium SOQLSubquery

23. Accounts That Have at Least One Prospecting Opportunity

Problem #134 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve the names of all Accounts that have at least one Opportunity in the "Prospecting" stage, using a semi-join to avoid duplicate rows.

Requirements

  • Return each qualifying Account's Id, Name, and Industry.
  • Use a semi-join against Opportunity, filtered to the "Prospecting" stage.
  • Sort the result alphabetically by Name.
Approach
  • 1A semi-join on AccountId, filtered to Opportunity's 'Prospecting' stage, finds the qualifying Accounts.
  • 2Returns unique Account rows even if an Account has multiple Prospecting Opps.
  • 3Outer query is FROM Account; inner subquery is FROM Opportunity.
Medium SOQLChild-to-Parent

24. SOQL Child-to-Parent: Opportunity with Account Owner (2-Level)

Problem #188 · Salesforce Apex Coding Challenge

Problem Statement

Write a SOQL query that traverses two levels of parent relationships: Opportunity → Account → Owner.

Requirements

  • Return each Opportunity's Id, Name, StageName, and Amount, plus its parent Account's Name and the Account Owner's Name.
  • Only include Opportunities that are still open.
  • Sort by the parent Account's Name, and cap the result at 20 rows.

Key Concept

SOQL supports up to 5 levels of parent traversal using chained dot notation: Account.Owner.Name goes Opportunity → Account → User (Owner).

Approach
  • 1Chain dot notation two levels deep to reach the Account's Owner (Opportunity → Account → User)
  • 2SOQL allows up to 5 levels of parent traversal
  • 3Filter to Opportunities that are not yet closed
Medium SOQLChild-to-Parent

25. SOQL Child-to-Parent: Filter Contacts by Account Type

Problem #189 · Salesforce Apex Coding Challenge

Problem Statement

Write a child-to-parent query that filters Contacts by their parent Account's Type field.

Requirements

  • Return each Contact's Id, FirstName, LastName, Email, and Phone, plus their parent Account's Name and Type.
  • Only include Contacts whose Account's Type is "Customer".
  • Sort by LastName, and cap the result at 50 rows.
Approach
  • 1Filter on the parent Account's Type field using dot notation in the WHERE clause
  • 2Include the parent Account's Type field in your SELECT to display it in results
  • 3Account.Type common values: Customer, Partner, Prospect, Other
Medium SOQLChild-to-Parent

26. SOQL Child-to-Parent: Case with Contact and Account Names

Problem #190 · Salesforce Apex Coding Challenge

Problem Statement

Write a SOQL query that retrieves Cases with fields from two different parent objects: Contact and Account.

Requirements

  • Return each Case's Id, CaseNumber, Subject, and Status, plus its Contact's first/last name and email, and its Account's name and phone.
  • Only include Cases that are not yet closed.
  • Sort by CaseNumber, and cap the result at 20 rows.
Approach
  • 1Case has two parent lookups: ContactId (to Contact) and AccountId (to Account)
  • 2Reach each parent's fields with dot notation off its own relationship name
  • 3Both parent traversals belong in the same query
Medium SOQLChild-to-Parent

27. SOQL Child-to-Parent: Filter Opportunities by Account Revenue

Problem #191 · Salesforce Apex Coding Challenge

Problem Statement

Write a query that returns open Opportunities where the parent Account's AnnualRevenue meets a minimum threshold.

Requirements

  • Return each Opportunity's Id, Name, StageName, Amount, and CloseDate, plus its parent Account's Name and AnnualRevenue.
  • Only include still-open Opportunities whose parent Account's AnnualRevenue meets a $1,000,000 minimum threshold.
  • Sort by the parent Account's AnnualRevenue, largest first, and cap the result at 20 rows.
Approach
  • 1Filter on the parent Account's numeric AnnualRevenue field using dot notation
  • 2Combine the revenue threshold and the open-status filter with AND
  • 3Sort descending by the parent Account's AnnualRevenue to see largest accounts first
Medium SOQLChild-to-Parent

28. SOQL Child-to-Parent: Contact Directory with Multiple Account Fields

Problem #192 · Salesforce Apex Coding Challenge

Problem Statement

Build a contact directory query that retrieves multiple Account fields alongside contact details for a richer data set.

Requirements

  • Return each Contact's Id, FirstName, LastName, Email, Phone, and Title, along with the parent Account's Name, Type, BillingCity, and Website.
  • Only include contacts whose parent Account has a Name populated.
  • Sort primarily by the Account's Name, then by the contact's LastName, and cap the results at 50 rows.
Approach
  • 1You can select several fields from the same parent relationship in one query, e.g. Account.Name alongside other Account fields.
  • 2ORDER BY can reference multiple fields, including parent fields, separated by commas.
  • 3Filtering a parent field for non-null values ensures every returned contact actually has an associated account.
Hard SOQLChild-to-Parent

29. SOQL Child-to-Parent: Contact with Account Owner (2-Level)

Problem #193 · Salesforce Apex Coding Challenge

Problem Statement

Write a query with two-level parent traversal: Contact → Account → Owner (User), retrieving the account owner's name and email.

Requirements

  • Return each Contact's Id, FirstName, LastName, and Email, plus the parent Account's Name and the Account owner's Name and Email.
  • Only include contacts whose account owner has a name populated.
  • Sort primarily by the account owner's name, then by the contact's LastName, and cap the results at 30 rows.
Approach
  • 1Dot notation can chain across more than one relationship step to reach a grandparent field, such as the owner of a contact's account.
  • 2Filters and sorts can also reference these multi-level relationship fields, not just top-level fields.
  • 3Think about which field represents the 'grandparent' value you need to sort by.
Medium SOQLParent-to-Child

30. SOQL Parent-to-Child: Account with Open Cases

Problem #196 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve Accounts that have open Cases, using a parent-to-child subquery for the cases and a semi-join to filter only accounts with open cases.

Requirements

  • For each qualifying Account, return its Id, Name, and Phone, plus a nested list of its non-closed cases' Id, CaseNumber, Subject, Status, and Priority, sorted by priority.
  • Restrict the outer Accounts to only those that actually have at least one non-closed case, using a filter against the Case object.
  • Sort the outer results alphabetically by Name and cap them at 10 rows.
Approach
  • 1The subquery's relationship name is the plural form of the child object.
  • 2A semi-join filters the outer object by checking membership against the result of an inner query on a related object — useful when you need to guarantee the parent actually has a matching child record.
  • 3The semi-join ensures you only get Accounts that actually have matching case records, rather than accounts with an empty nested list.
Medium SOQLParent-to-Child

31. SOQL Parent-to-Child: Account with Minimal Contact Subquery

Problem #197 · Salesforce Apex Coding Challenge

Problem Statement

Write a parent-to-child query that retrieves Accounts with only the Contact Id in the subquery — the minimal useful subquery pattern.

Requirements

  • Return each qualifying Account's Id, Name, and Type, plus just the Id of each related contact in a nested list.
  • Restrict the results to Accounts that actually have at least one related contact.
  • Sort alphabetically by Name and cap the results at 20 rows.
Approach
  • 1A child subquery doesn't need to select many fields — sometimes just the Id is enough to prove a relationship exists.
  • 2A semi-join against the child object lets you keep only parent records that actually have a matching child, filtering by the parent's own Id.
  • 3The child subquery's relationship name is plural, while a semi-join filter on the child object itself uses the singular object name.
Medium SOQLParent-to-Child

32. SOQL Parent-to-Child: Account with Contacts Filtered by Email

Problem #198 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve Accounts with a subquery for Contacts that have an email address. Filter and sort within the subquery.

Requirements

  • Return each Account's Id, Name, and Industry, plus a nested list of its contacts that have an email address on file, showing each contact's Id, FirstName, LastName, and Email.
  • Sort the nested contacts alphabetically by LastName.
  • Sort the outer Account results alphabetically by Name and cap them at 15 rows.
Approach
  • 1A subquery can carry its own filter condition to restrict which child records are included in the nested list.
  • 2Add a sort inside the subquery to order the nested child records independently of the outer query.
  • 3Accounts without any contacts that meet the filter will simply have an empty nested list, not an error.
Medium SOQLParent-to-Child

33. SOQL Parent-to-Child: Account with Opportunities Ordered by Close Date

Problem #199 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve high-revenue Accounts with their Opportunities sorted by close date. Combines outer-query filtering with inner-subquery sorting.

Requirements

  • Return each Account's Id, Name, and AnnualRevenue, plus a nested list of its opportunities' Id, Name, StageName, Amount, and CloseDate, sorted by close date (soonest first).
  • Restrict the outer results to Accounts with a positive annual revenue.
  • Sort the outer results by AnnualRevenue from highest to lowest, and cap them at 10 rows.
Approach
  • 1A subquery's sort order operates independently of the outer query's sort order — each can order by a different field and direction.
  • 2The outer query's sort can be on a totally different field than the one used to filter it.
  • 3A simple numeric comparison filter can restrict the outer query to accounts above a threshold.
Medium SOQLParent-to-Child

34. SOQL Parent-to-Child: Account with Contacts Sorted NULLS LAST

Problem #200 · Salesforce Apex Coding Challenge

Problem Statement

Retrieve Accounts with a Contacts subquery that sorts contacts by LastName using the NULLS LAST clause to handle contacts with no last name gracefully.

Requirements

  • Return each Account's Id, Name, and Type, plus a nested list of its contacts' Id, FirstName, LastName, and Title.
  • Sort the nested contacts alphabetically by LastName, placing contacts with no last name at the end of that list rather than the beginning.
  • Sort the outer Account results alphabetically by Name and cap them at 15 rows.
Approach
  • 1SOQL has a modifier for controlling where null values land in a sort order, appended after the ASC/DESC keyword.
  • 2By default, ascending sorts place nulls first — you can override that placement explicitly.
  • 3This modifier can be applied inside a child subquery's ORDER BY just like in an outer query.

Halfway there — solve them live

Every problem above runs in a real in-browser Apex editor with instant pass/fail feedback and best-practice linting. No Salesforce org needed.

Create Free Account →
Hard SOQLParent-to-Child

35. SOQL Parent-to-Child: Account with Two Subqueries (Contacts + Opportunities)

Problem #201 · Salesforce Apex Coding Challenge

Problem Statement

Write a query with two parent-to-child subqueries in a single SELECT, retrieving both Contacts and Opportunities for each Account.

Requirements

  • Include a nested list of each Account's contacts (Id, FirstName, LastName, Email), sorted alphabetically by LastName.
  • Also include a nested list of each Account's still-open opportunities (Id, Name, StageName, Amount).
  • Return each Account's Id, Name, Industry, and AnnualRevenue, restricted to Accounts with a populated Name, sorted alphabetically by Name and capped at 10 rows.
Approach
  • 1A single outer query can include more than one child subquery, separated by commas in the field list.
  • 2Each subquery can have its own independent filter, sort, and limit.
  • 3Keep the two subqueries targeting different related objects distinct from each other.
Hard SOQLParent-to-Child

36. SOQL Parent-to-Child: Account with Cases and Case Comments

Problem #202 · Salesforce Apex Coding Challenge

Problem Statement

Write a three-level nested query: Account → Cases → CaseComments, using two levels of parent-to-child subqueries.

Requirements

  • Within each case's nested list, include a further nested list of its comments (Id, CommentBody, CreatedDate), newest first.
  • For each Account, include a nested list of its non-closed cases (Id, CaseNumber, Subject, Status), sorted by case number, each carrying the nested comments described above.
  • Return each Account's Id and Name, sorted alphabetically by Name and capped at 5 rows.
Approach
  • 1SOQL supports nesting a subquery inside another subquery to traverse a grandchild relationship.
  • 2The relationship name for case comments follows the same plural naming pattern as other child relationships.
  • 3Build the innermost subquery first, then wrap it inside the subquery for its immediate parent object.
Hard SOQLChild-to-ParentParent-to-Child

37. SOQL Combined: Child-to-Parent + Parent-to-Child

Problem #203 · Salesforce Apex Coding Challenge

Problem Statement

Write two SOQL queries in the same editor — one using child-to-parent, one using parent-to-child — demonstrating both relationship directions.

Query 1 — Child-to-Parent

  • Return each open Opportunity's core fields along with its parent Account's Name and the account owner's Name, sorted by Amount from highest to lowest and capped at 20 rows.

Query 2 — Parent-to-Child

  • Return Accounts with a nested list of their contacts' core fields (sorted by LastName), capped at 10 rows.
Approach
  • 1Query 1: child-to-parent uses dot notation to traverse from a child record up through its parent (and grandparent) relationships.
  • 2Query 2: parent-to-child uses a subquery in parentheses within the outer SELECT to embed related child records.
Medium SOQL

38. SOQL: Aggregate — Accounts with 5 or More Contacts

Problem #215 · Salesforce Apex Coding Challenge

Problem Statement

Write a SOQL aggregate query that returns the AccountId and the number of Contacts (COUNT(Id)) for every Account that has 5 or more Contacts.

Requirements

  • Query the Contact object, excluding Contacts with no Account.
  • Group the results by Account so each Account appears once with its Contact count.
  • Alias the count as contactCount (the grading checks this exact alias).
  • Only keep Accounts whose Contact count meets the 5-or-more threshold.
  • Sort so the Accounts with the most Contacts appear first.
  • Limit to 20 results.

Key Concepts

  • GROUP BY — collapses rows by a field value.
  • HAVING — filters after grouping (unlike WHERE which filters before).
  • Aggregate functions: COUNT(Id), SUM(), AVG(), MAX(), MIN().
Approach
  • 1The count alias must be exactly contactCount — grading checks for it.
  • 2Filtering on the group total (rather than individual rows) requires a clause that runs after grouping, not a plain WHERE.
  • 3Sort using the same aggregate expression you grouped/filtered on, descending.
  • 4HAVING vs WHERE: WHERE filters individual rows before grouping; HAVING filters groups after.
Medium SOQL

39. SOQL: Semi-Join and Anti-Join Subqueries

Problem #216 · Salesforce Apex Coding Challenge

Problem Statement

Write two SOQL queries demonstrating semi-join and anti-join patterns.

Query 1 — Semi-Join (IN subquery)

  • Select the Id, FirstName, LastName, and Email of Contacts whose Account is in the Technology industry, using a semi-join rather than a dot-notation traversal.
  • Sort alphabetically by LastName and cap the results at 50 rows.

Query 2 — Anti-Join (NOT IN subquery)

  • Select the Id, Name, Industry, and AnnualRevenue of Accounts that have no related Contacts at all, using an anti-join.
  • Sort alphabetically by Name and cap the results at 25 rows.

Key Rules

  • The subquery in an anti-join must exclude records where the join field is null, otherwise the NOT IN comparison returns no rows at all.
  • Semi-join / anti-join subqueries can only return one field.
Approach
  • 1A semi-join filters the outer object by checking that its Id (or a lookup field) appears in the result of an inner query on a related object.
  • 2An anti-join is the semi-join's mirror image: it filters the outer object by checking that its Id does NOT appear in the inner query's result.
  • 3Critical: the anti-join's inner subquery must filter out null values on the join field it selects — NOT IN with nulls in the comparison set always returns 0 rows.
  • 4Subqueries in semi/anti-join can only SELECT a single Id field.
Medium SOQL

40. SOQL: Dynamic SOQL with String.escapeSingleQuotes

Problem #228 · Salesforce Apex Coding Challenge

Problem Statement

Write AccountSearchService with a static method searchByName(String searchTerm) that performs a dynamic SOQL search on Account using LIKE.

Requirements

  • Return an empty list if searchTerm is blank.
  • Sanitise input: String.escapeSingleQuotes(searchTerm.trim()).
  • Wrap with wildcards: '%' + safeTerm + '%'.
  • Build the SOQL string using string concatenation with a bind variable (:safeTerm).
  • Execute with Database.query(soql).
  • Wrap in try-catch(QueryException e) — return empty list on error.
  • Return the fields Id, Name, Industry, AnnualRevenue, Phone, capped at 50 records.

Best Practices

  • String.escapeSingleQuotes() is mandatory for dynamic SOQL — prevents SOQL injection attacks.
  • Catch QueryException specifically, not generic Exception, for SOQL errors.
  • Return an empty list (not null) on error — callers should never receive null.
Approach
  • 1Blank check: if (String.isBlank(searchTerm)) return new List();
  • 2Escape: String safeTerm = '%' + String.escapeSingleQuotes(searchTerm.trim()) + '%';
  • 3Bind variables work in dynamic SOQL strings too — reference the local `safeTerm` variable with a colon, and it is resolved at runtime.
  • 4Execute: return (List) Database.query(soql); — cast is required.
Hard SOQL

41. SOQL: Aggregate with CALENDAR_MONTH and FISCAL_YEAR Date Functions

Problem #229 · Salesforce Apex Coding Challenge

Problem Statement

Write two advanced aggregate SOQL queries using date functions.

Query 1 — Monthly Revenue Breakdown

  • For each calendar month, return the total revenue and count of won Opportunities that closed in that month, aliasing the aggregated columns.
  • Restrict the results to won opportunities that closed within the current fiscal year.
  • Group by month and sort chronologically.

Query 2 — Deal Size Stats by Stage

  • For each Stage, return the average, highest, and lowest Amount among closed opportunities.
  • Restrict the results to opportunities that closed within the last 90 days.
  • Group by stage and sort by the average deal size, largest first.

Key Concepts

  • CALENDAR_MONTH(), FISCAL_YEAR() — SOQL date functions usable in SELECT and GROUP BY.
  • SOQL provides relative date literals for "current fiscal year" and "the last N days" so you don't need to compute date boundaries yourself.
  • AVG(), MAX(), MIN() — aggregate functions.
Approach
  • 1Date functions can appear in the SELECT list just like regular fields, and can be given an alias for a friendlier column name.
  • 2SOQL has a date function for extracting the fiscal year of a date field, which can be compared against a relative date literal representing the current fiscal year — no bind variable needed.
  • 3SOQL supports a relative date literal for "the last N days" using a colon-separated syntax, letting you avoid computing a date boundary manually.
  • 4Whatever date function you group by must also appear (in the same form) in the SELECT list.
Medium SOQLAggregate

42. SOQL: Sum of All Opportunity Amounts

Problem #260 · Salesforce Apex Coding Challenge

Problem Statement

Implement getTotalOpportunityAmount() that returns the sum of the Amount field across all Opportunities.

Requirements

  • Use SUM(Amount) with alias totalAmount
  • FROM Opportunity
  • Access the result with results[0].get('totalAmount')
  • Cast and return as Decimal
Approach
  • 1SUM(Amount) totalAmount — the alias is used when calling .get('totalAmount').
  • 2AggregateResult[] holds the result; results[0] is the single summary row.
  • 3Cast with (Decimal) since .get() returns Object.
Medium SOQLAggregate

43. SOQL: Average Amount of All Opportunities

Problem #261 · Salesforce Apex Coding Challenge

Problem Statement

Implement getAverageOpportunityAmount() that returns the average of the Amount field across all Opportunities.

Requirements

  • Use AVG(Amount) with alias avgAmount
  • FROM Opportunity
  • Access via results[0].get('avgAmount')
  • Return type: Decimal
Approach
  • 1AVG(Amount) calculates the mean; null Amount records are excluded from the average.
  • 2Alias the result (avgAmount) so .get('avgAmount') retrieves it cleanly.
  • 3AVG returns a Decimal — always cast the Object result.
Medium SOQLAggregate

44. SOQL: Maximum Opportunity Amount

Problem #262 · Salesforce Apex Coding Challenge

Problem Statement

Implement getMaxOpportunityAmount() that returns the largest Amount value across all Opportunities.

Requirements

  • Use MAX(Amount) with alias maxAmount
  • FROM Opportunity
  • Access via results[0].get('maxAmount')
  • Return type: Decimal
Approach
  • 1MAX(Amount) finds the single highest value in the Amount column.
  • 2Like all aggregate functions, MAX ignores null values.
  • 3Pair with MIN() to get the full range.
Medium SOQLAggregate

45. SOQL: Minimum Opportunity Amount

Problem #263 · Salesforce Apex Coding Challenge

Problem Statement

Implement getMinOpportunityAmount() that returns the smallest Amount value across all Opportunities.

Requirements

  • Use MIN(Amount) with alias minAmount
  • FROM Opportunity
  • Access via results[0].get('minAmount')
  • Return type: Decimal
Approach
  • 1MIN(Amount) finds the single lowest value in the Amount column.
  • 2Null Amount records are excluded from MIN/MAX calculations.
  • 3Both MIN and MAX work on numeric, date, and text fields.
Medium SOQLAggregate

46. SOQL: Count Opportunities Grouped by Stage

Problem #264 · Salesforce Apex Coding Challenge

Problem Statement

Implement getOpportunityStageCount() that returns a count of Opportunities grouped by StageName.

Requirements

  • SELECT StageName, COUNT(Id) totalCount
  • FROM Opportunity
  • GROUP BY StageName
  • Return type: List<AggregateResult>

Accessing Results

for (AggregateResult ar : results) {
    String stage = (String)  ar.get('StageName');
    Integer cnt  = (Integer) ar.get('totalCount');
}
Approach
  • 1GROUP BY StageName splits rows into one summary row per unique stage.
  • 2COUNT(Id) counts records in each group — alias it as totalCount.
  • 3Every non-aggregate SELECT field must appear in the GROUP BY clause.
Medium SOQLAggregate

47. SOQL: Filter Groups With HAVING Clause

Problem #265 · Salesforce Apex Coding Challenge

Problem Statement

Implement getStagesHavingMoreThanFiveOpps() that returns only Opportunity stages that have more than 5 records.

Requirements

  • SELECT StageName, COUNT(Id) totalCount
  • GROUP BY StageName
  • HAVING COUNT(Id) > 5

Key Concept

HAVING filters after grouping (like a WHERE for aggregate results). It cannot be replaced by WHERE because WHERE filters individual rows, not groups.

Approach
  • 1HAVING filters groups — it operates on the result of GROUP BY.
  • 2WHERE filters individual rows before grouping; HAVING filters groups after.
  • 3HAVING COUNT(Id) > 5 keeps only stages with more than five opportunities.
Medium SOQLRelationships

48. SOQL: Child-to-Parent Relationship — Contact to Account Name

Problem #266 · Salesforce Apex Coding Challenge

Problem Statement

Implement getContactsWithAccountName() that retrieves all Contacts along with the parent Account's Name using a child-to-parent relationship traversal.

Requirements

  • Return the Id, FirstName, and LastName of every Contact
  • Also include the related Account's Name, reached by traversing the lookup from Contact to Account
  • Query the Contact object

Dot Notation

Traverse up to 5 levels of parent relationships using dots: Contact.Account.Owner.Profile.Name.

Approach
  • 1Dot notation traverses parent relationships: Account.Name fetches the related Account's Name.
  • 2The relationship name from Contact to Account is "Account" (same as the field name).
  • 3Up to 5 levels deep is allowed: Contact.Account.Owner.Name.
Medium SOQLRelationships

49. SOQL: Child-to-Parent Relationship — Opportunity to Account Name

Problem #267 · Salesforce Apex Coding Challenge

Problem Statement

Implement getOppsWithAccountName() that retrieves all Opportunities along with the parent Account's Name.

Requirements

  • Return the Id and Name of every Opportunity
  • Also include the related Account's Name, reached by traversing the lookup to Account
  • Query the Opportunity object
Approach
  • 1Opportunity has a standard lookup to Account via the AccountId field.
  • 2Use Account.Name in the SELECT clause to traverse the relationship.
  • 3The resulting Opportunity records will have opp.Account.Name populated.
Hard SOQLRelationships

50. SOQL: Multi-Level Child-to-Parent — Opportunity Owner Profile

Problem #268 · Salesforce Apex Coding Challenge

Problem Statement

Implement getOpportunityOwnerProfile() that traverses two levels of parent relationships to retrieve the Opportunity owner's name and their Profile name.

Requirements

  • Return the Id and Name of every Opportunity
  • Also include the owner's name and the owner's Profile name, reached by traversing two levels of parent relationships
  • Query the Opportunity object

Relationship Chain

An Opportunity's owner is a User, and that User has a related Profile — chaining a relationship traversal across both hops lets you reach the Profile's Name from the Opportunity.

Approach
  • 1Look up the relationship name for the OwnerId lookup field on Opportunity — it lets you reach the owning User record.
  • 2The User object has its own lookup to a Profile record — chain a second dot-notation hop off the first to reach it.
  • 3Up to 5 levels of parent traversal are allowed in a single SOQL query.
Medium SOQLSubquery

51. SOQL: Parent-to-Child Subquery — Account With Contacts

Problem #269 · Salesforce Apex Coding Challenge

Problem Statement

Implement getAccountsWithContacts() using a parent-to-child subquery to fetch each Account along with its related Contacts in a single query.

Requirements

  • Return each Account's Id and Name
  • For each Account, also include a nested list of its related Contacts, each with Id, FirstName, and LastName
  • Use the plural child relationship name when writing the nested query

Key Concept

The child relationship name is the plural of the child object: Contacts, Opportunities, Cases, etc.

Approach
  • 1Subqueries use the plural child relationship name: Contacts (not Contact).
  • 2The subquery is wrapped in parentheses inside the outer SELECT.
  • 3Access child records: for (Contact c : account.Contacts) { … }
Medium SOQLSubquery

52. SOQL: Parent-to-Child Subquery — Account With Opportunities

Problem #270 · Salesforce Apex Coding Challenge

Problem Statement

Implement getAccountsWithOpportunities() using a parent-to-child subquery to retrieve each Account with its related Opportunities.

Requirements

  • Return each Account's Id and Name
  • For each Account, also include a nested list of its related Opportunities, each with Id, Name, and Amount
  • Note the plural child relationship name for Opportunity records
Approach
  • 1The child relationship name from Account to Opportunity is "Opportunities" (plural).
  • 2Subquery fields: Id, Name, Amount — keep it minimal.
  • 3Access: for (Opportunity o : account.Opportunities) { … }
Medium SOQLSubquery

53. SOQL: Parent-to-Child Subquery With WHERE Filter

Problem #271 · Salesforce Apex Coding Challenge

Problem Statement

Implement getAccountsWithClosedWonOpps() that fetches every Account together with only its Closed Won Opportunities using a filtered parent-to-child subquery.

Requirements

  • Return every Account's Id and Name, regardless of its Opportunities
  • For each Account, include a nested list of only its Opportunities that are in the "Closed Won" stage, each with Id, Name, and StageName
Approach
  • 1You can add a WHERE clause inside the child subquery to filter child records.
  • 2Filter the nested subquery so only Opportunities in the "Closed Won" stage are returned.
  • 3The outer Account records are all returned — only the child list is filtered.
Hard SOQLSubquery

54. SOQL: Semi-Join — Accounts That Have Opportunities

Problem #273 · Salesforce Apex Coding Challenge

Problem Statement

Implement getAccountsHavingOpps() using a semi-join to return only Accounts that have at least one related Opportunity.

Requirements

  • Return only the Id and Name of Accounts that have at least one related Opportunity
  • Filter the outer Account query by checking membership against an inner query on the Opportunity object that returns the linking AccountId

Semi-Join vs Subquery

A semi-join uses IN (SELECT …) in the WHERE clause to filter the outer object based on existence in a related object — no JOIN keyword needed in SOQL.

Approach
  • 1A semi-join filters the outer object using IN against an inner query on a related object — no outer JOIN needed.
  • 2The inner SELECT must return the field that links back to the outer object (AccountId).
  • 3SOQL supports up to 2 levels of subquery nesting.
Hard SOQLSubquery

55. SOQL: Anti-Join — Accounts Without Any Opportunities

Problem #274 · Salesforce Apex Coding Challenge

Problem Statement

Implement getAccountsWithoutOpps() using an anti-join to return only Accounts that have no related Opportunities.

Requirements

  • Return only the Id and Name of Accounts that have zero related Opportunities
  • Filter the outer Account query by excluding any match against an inner query on the Opportunity object that returns the linking AccountId

Anti-Join vs Semi-Join

  • Semi-join: IN (subquery) — records that have a match
  • Anti-join: NOT IN (subquery) — records that do NOT have a match
Approach
  • 1NOT IN (subquery) is an anti-join — it returns the complement of a semi-join.
  • 2A null AccountId in Opportunity can cause unexpected results with NOT IN — be aware.
  • 3This pattern is essential for finding orphaned records (e.g., Accounts with no deals).
Medium SOQLAggregate

56. SOQL: Total Opportunity Amount Grouped by Stage

Problem #276 · Salesforce Apex Coding Challenge

Problem Statement

Implement getOpportunityAmountByStage() that returns the total Opportunity amount for each stage.

Requirements

  • SELECT StageName, SUM(Amount) totalAmount
  • FROM Opportunity
  • GROUP BY StageName
  • Return type: List<AggregateResult>

Accessing Results

Decimal total = (Decimal) ar.get('totalAmount');
String stage  = (String)  ar.get('StageName');
Approach
  • 1Combine SUM with GROUP BY to get a total per stage.
  • 2Alias the SUM: SUM(Amount) totalAmount so you can retrieve it with .get('totalAmount').
  • 3Non-aggregate SELECT fields (StageName) must appear in GROUP BY.
Medium SOQLDynamic SOQL

57. SOQL: Dynamic SOQL Using Database.query()

Problem #279 · Salesforce Apex Coding Challenge

Problem Statement

Implement getDynamicAccounts(String industryName) that builds a SOQL query dynamically at runtime and executes it with Database.query().

Requirements

  • Build a SOQL query string, as text, that selects the Id, Name, and Industry of Accounts filtered by the given industryName
  • Use a bind variable so industryName is substituted safely, not concatenated as raw text
  • Execute the built string with Database.query(queryStr)
  • Return type: List<Account>

Security Note

Using :variable bind syntax in dynamic SOQL is safe. Never concatenate user input directly into the query string — that creates SOQL injection vulnerabilities.

Approach
  • 1Concatenate string parts to build the query: 'SELECT … ' + 'FROM Account ' + 'WHERE …'.
  • 2Bind variables in dynamic SOQL still use :variableName — they are resolved from the Apex scope.
  • 3Database.query(queryStr) executes the dynamic string and returns a List; cast as needed.
Medium SOQL

58. SOQL: Count Distinct Opportunity Owners per Stage

Problem #396 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class OpportunityOwnerStageQuery with a method getDistinctOwnerCountByStage() that returns, for each StageName, the number of distinct Opportunity Owners who have an Opportunity in that stage.

Requirements

  • Use COUNT_DISTINCT(OwnerId) — a plain COUNT(OwnerId) would count every Opportunity, not every unique owner.
  • Group the results by StageName, aliasing both the grouped field and the aggregate.
  • Return type is List<AggregateResult>.

Best Practices

  • COUNT_DISTINCT vs plain COUNT is a common SOQL gotcha — pick the one that actually answers the business question ("how many opportunities" vs "how many different owners").
  • Alias both the grouped field and the aggregate expression so calling code can read results by name instead of positional index.
  • Order grouped results deterministically (ORDER BY StageName ASC) so output doesn't depend on database internals.
Approach
  • 1COUNT_DISTINCT(OwnerId) counts unique owners; COUNT(OwnerId) would count every Opportunity row instead.
  • 2Alias the grouped field (stage) and the aggregate (ownerCount) so results are easy to read by name.
  • 3The field you group by must be the exact same field you aliased as stage in the SELECT list.
Hard SOQL

59. SOQL: Multi-Field GROUP BY With HAVING — High-Revenue Stage/Type Combinations

Problem #397 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class StageTypeRevenueQuery with a method getHighRevenueStageTypeCombos() that groups Opportunities by both StageName and Type, totals the Amount for each combination, and returns only combinations whose total exceeds a revenue threshold.

Requirements

  • Group by both StageName and Type together — two grouping fields, not one.
  • Keep only combinations whose summed Amount exceeds the threshold, where the threshold is a named constant, not a raw literal in the query.
  • Order the result by total descending, so the highest-revenue combination comes first.

Best Practices

  • Grouping by more than one field is straightforward — just list every grouping column, separated by commas, in both SELECT and GROUP BY.
  • HAVING filters on the aggregated value; a plain WHERE cannot reference SUM(Amount).
  • A business-meaningful threshold like this belongs in a named constant (REVENUE_THRESHOLD) rather than a bare number, even inside a query.
Approach
  • 1List every grouping field, comma-separated, in both the SELECT list and the grouping clause.
  • 2HAVING comes after the grouping clause and can reference the aggregate function directly to filter on the summed total against the threshold constant.
  • 3You can order by an aggregate expression the same way you would a regular field, sorting the largest totals first.
Hard SOQL

60. SOQL: Leads Without a Matching Converted Contact by Email

Problem #398 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class UnmatchedLeadQuery with a method getLeadsWithoutMatchingContact() that returns every non-converted Lead whose Email does not match any existing Contact's Email.

Requirements

  • Use an anti-join (NOT IN with a subquery) against Contact's Email field — not an Apex loop.
  • Filter out Leads with a null Email, and filter the inner subquery to Email != null Contacts too.
  • Only consider non-converted Leads (IsConverted = false).

Best Practices — the NOT IN / NULL pitfall

  • In SQL-family languages (SOQL included), X NOT IN (subquery) that returns even one null makes the whole comparison evaluate to unknown/false for every row — the outer query would silently return nothing. Filtering the subquery to Email != null avoids this trap entirely.
  • Guarding the outer Email != null too avoids comparing a null Lead email against the subquery at all.
  • WITH SECURITY_ENFORCED plus LIMIT round out the query as production-safe.
Approach
  • 1A NOT IN subquery that can return a null value makes the whole WHERE clause match nothing — always filter the subquery's column to != null.
  • 2Guard the outer Email != null too, so Leads with no email at all aren't evaluated against the subquery.
  • 3IsConverted = false keeps already-converted Leads (which already became Contacts) out of scope.
Medium SOQL

61. SOQL: Case Count by Priority for a Specific Account

Problem #407 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class CasePriorityCountQuery with a method getCaseCountByPriority(Id accountId) that returns, for a single Account, how many Cases exist at each Priority level.

Requirements

  • Scope the aggregation to a single Account, identified by the accountId parameter, before grouping.
  • Group the results by Priority, aliasing both the grouped field and the count.

Best Practices

  • Filtering with WHERE before an aggregate GROUP BY scopes the aggregation to exactly the records you care about — the WHERE clause applies before grouping, not after.
  • Alias both the grouped dimension and the aggregate so results read naturally by name.
Approach
  • 1Filtering to the given Account before grouping ensures only that Account's Cases are counted per Priority.
  • 2Alias the grouped field (pri) and the aggregate (caseCount) so the AggregateResult keys are readable.
  • 3ORDER BY Priority ASC gives deterministic, alphabetically sorted output.
Medium SOQL

62. SOQL: Contacts With Their Account's Annual Revenue

Problem #408 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class ContactAccountRevenueQuery with a method getContactsWithAccountRevenue() that returns every Contact together with its parent Account's Name and AnnualRevenue, using a child-to-parent relationship query.

Requirements

  • Traverse the relationship with dot notation: Account.Name, Account.AnnualRevenue — no second query needed.
  • Only include Contacts that actually have a parent Account.

Best Practices

  • A single child-to-parent dot-notation query avoids an unnecessary second SOQL query and a manual map-join — the platform resolves the relationship for you.
  • Excluding Contacts with no parent Account guards against every Account.* field coming back null on those rows.
Approach
  • 1Account.Name and Account.AnnualRevenue can be selected directly with dot notation — no second query required.
  • 2Excluding Contacts whose AccountId is null avoids returning empty relationship fields for orphaned Contacts.
  • 3This still returns List — the parent fields just come along for the ride on each row.
Medium SOQL

63. SOQL: Each Account's Highest-Value Open Opportunity

Problem #409 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class AccountTopOpportunityQuery with a method getAccountsWithTopOpenOpportunity() that returns every Account together with (at most) its single highest-value open Opportunity, using a parent-to-child subquery.

Requirements

  • The subquery must consider only Opportunities that are still open, and narrow that set down to just the single one with the largest Amount per Account.
  • Return type is List<Account> — each Account's related list will contain zero or one Opportunity.

Best Practices

  • A subquery can carry its own independent WHERE, ORDER BY, and LIMIT — this is the standard way to fetch "top N related records per parent" without a second query or in-memory sorting.
  • This single query replaces what would otherwise require querying Accounts, then looping to query each Account's Opportunities separately — exactly the kind of query-in-a-loop this pattern avoids.
Approach
  • 1A parent-to-child subquery goes inside parentheses right in the SELECT list, referencing the child relationship name.
  • 2The subquery can define its own filtering, sorting, and row cap that are entirely independent of the outer query's clauses.
  • 3This returns List — access each Account's matched Opportunity via its Opportunities relationship list in calling code.
Hard SOQL

64. SOQL: Two-Level Child-to-Parent — Contact's Account Owner Name

Problem #410 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class ContactAccountOwnerQuery with a method getContactsWithAccountOwner() that returns every Contact together with the Name and Email of its parent Account's Owner — a two-level child-to-parent traversal (Contact → Account → Owner).

Requirements

  • Use a two-level dot-notation path: Account.Owner.Name, Account.Owner.Email.
  • Guard against Contacts with no parent Account.

Best Practices

  • SOQL supports traversing up to 5 levels of parent relationships via dot notation — Account.Owner.Name is a two-level hop (Contact → Account → Owner), all resolved in a single query with no additional round trips.
  • The same null-Account guard matters even more here, since a null Account also means a null Owner two levels up.
Approach
  • 1Chain the relationship two levels deep: Account.Owner.Name reaches the User who owns the Contact's Account.
  • 2SOQL allows up to 5 levels of parent-relationship traversal via dot notation in a single query.
  • 3The null-Account guard still applies here — without a parent Account there is no Owner to reach either.
Medium SOQL

65. SOQL: Opportunities Missing a Next Step

Problem #411 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class OpenOpportunityGapsQuery with a method getOpenOpportunitiesMissingNextStep() that returns every open Opportunity that has no NextStep defined — a common "sales hygiene" report.

Requirements

  • Only open Opportunities should be included.
  • Only those with a blank/null NextStep.

Best Practices

  • In SOQL, comparing a field to null is written as Field = null (or != null) — this is the SOQL equivalent of Apex's String.isBlank() check, since SOQL doesn't have a text-comparison function for blank/whitespace-only text fields.
  • Combining the open-status check with the blank check in one query avoids a wasteful "fetch everything then filter in Apex" pattern.
Approach
  • 1Filtering on the standard open/closed status field scopes the query to open Opportunities only.
  • 2A blank text field is checked in SOQL with a null comparison — there is no ISBLANK() function in SOQL itself.
  • 3Combine both conditions with AND in a single WHERE clause.
Hard SOQL

66. SOQL: Accounts With More Than One Open Opportunity (GROUP BY + HAVING)

Problem #412 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class MultiOpenOpportunityAccountsQuery with a method getAccountsWithMultipleOpenOpportunities() that returns each Account's open Opportunity count, but only for Accounts with more than one open Opportunity — ranked from most to fewest.

Requirements

  • Filter to open Opportunities with WHERE IsClosed = false before grouping.
  • GROUP BY AccountId, then use HAVING COUNT(Id) > 1 to keep only multi-opportunity Accounts.
  • Order by the count, descending.

Best Practices — WHERE vs. HAVING

  • WHERE filters individual rows before they're grouped; HAVING filters groups after aggregation. You cannot write WHERE COUNT(Id) > 1 — the aggregate doesn't exist yet at the row-filtering stage, which is exactly why HAVING exists as a separate clause.
  • Ordering by the same aggregate expression used in HAVING keeps the most significant Accounts at the top of the result.
Approach
  • 1WHERE IsClosed = false filters rows before grouping; it cannot reference COUNT(Id).
  • 2HAVING COUNT(Id) > 1 filters the grouped results after aggregation — that's the only place a condition on an aggregate can go.
  • 3ORDER BY COUNT(Id) DESC works even though the SELECT list aliased it as oppCount — both refer to the same aggregate.
Hard SOQL

67. SOQL: Accounts With Both Open Opportunities and Contacts

Problem #414 · Salesforce Apex Coding Challenge

Problem Statement

Write an Apex class ActiveAccountsWithContactsQuery with a method getActiveAccountsWithContacts() that returns every Account that both has at least one open Opportunity and has at least one Contact — along with the list of those Contacts.

Requirements

  • Use a semi-join against Opportunity to require at least one open Opportunity.
  • Use a second, separate semi-join against Contact to require at least one Contact.
  • Also include each qualifying Account's Contacts via a parent-to-child subquery in the SELECT list.

Best Practices

  • Two semi-joins combined with AND is how SOQL expresses "must have a related record satisfying condition A and a related record satisfying condition B" without pulling everything into Apex and filtering there.
  • The parent-to-child subquery that shapes the returned Contacts data is independent from the semi-join subqueries in the WHERE clause — one shapes the returned data, the others just filter which Accounts qualify.
Approach
  • 1A semi-join filters the outer object by whether a related child record exists — no Apex loop needed.
  • 2Combine two semi-joins with AND to require both conditions to hold at once.
  • 3The Contacts subquery that shapes the outer SELECT list is separate from the semi-join subqueries used for filtering.
Hard SOQL

68. Scenario: Multi-Currency-Aware Opportunity Revenue Rollup

Problem #445 · Salesforce Apex Coding Challenge

Business Scenario

This org has multi-currency enabled — Closed Won Opportunities are recorded in whatever currency the deal was closed in. Finance needs one combined total in USD across every currency.

Requirements

  • Write an Apex class MultiCurrencyRevenueQuery with a method getTotalWonRevenueInUSD(Map<String, Decimal> rateToUsdByCurrencyCode) taking a currency-code-to-USD-conversion-rate map (e.g. {'EUR' => 1.08}).
  • Aggregate Closed Won Amount, grouped by CurrencyIsoCode, in a single query.
  • Convert each currency's subtotal to USD using the supplied rate map, and sum the results.
  • If a currency code isn't in the rate map, treat its rate as 1 (a safe fallback) rather than throwing.

Best Practices

  • Aggregating separately per currency is the standard way to handle amounts in a multi-currency org — summing raw numbers across different currencies would silently treat them as if they were the same unit.
  • Currency conversion belongs in Apex (using externally-supplied, up-to-date rates), not hardcoded — this method accepts the rate map as a parameter rather than hardcoding rates.
Approach
  • 1Aggregating separately per currency code keeps each currency's Closed Won total distinct — never mix raw amounts across currencies.
  • 2rateToUsdByCurrencyCode.containsKey(currencyCode) lets you fall back to a rate of 1 for any currency the caller didn't supply a rate for.
  • 3Accumulate subtotal * rate into a running total across every currency group.

Practice All 68 Problems Free

Write and run real Apex code right in your browser — instant pass/fail feedback, best-practice linting, and governor limit monitoring. No Salesforce org needed.

Create Free Account → Explore All Problems
More Practice Topics
About ApexArena

ApexArena is a free, browser-based Salesforce Apex coding practice platform covering every major topic tested on the Salesforce Platform Developer I (PD1) and Platform Developer II (PD2) certification exams. All problems run directly in your browser with instant pass/fail feedback, best-practice linting (SOQL in loops, DML in loops, empty catch blocks), and governor limit monitoring — no Salesforce Developer Edition org required.

Related tutorials: Apex Triggers · SOQL · Batch Apex · Interview Q&A · Governor Limits