Relationship & Aggregate SOQL, Subqueries, and Avoiding N+1 in Salesforce

Share

Fast, reliable Apex leans on three pillars: relationship queries, aggregate SOQL, and thoughtful subqueries—while steering clear of N+1 patterns. Nail these and your code stays bulkified, selective, and governor-limit friendly.


Relationship SOQL (child → parent): follow lookups with dot notation

Concept: Pull fields from a parent record in the same query by walking the relationship path (e.g., Contact.Account.Name).

Real-world example: A contact list that also shows the Account name and Account Owner—no follow-up queries needed.

public with sharing class ContactRepo {
    public static List<Contact> recentContacts(Integer limitSize) {
        return [
            SELECT Id, FirstName, LastName,
                   Account.Id, Account.Name,
                   Account.Owner.Id, Account.Owner.Name
            FROM Contact
            WHERE LastModifiedDate = LAST_N_DAYS:30
            ORDER BY LastModifiedDate DESC
            LIMIT :Math.min(limitSize, 200)
        ];
    }
}

Why it’s good: No extra round trips for Account or Owner—SOQL returns everything at once.


Parent → child subqueries: return child collections in one go

Concept: Nest a subquery in the SELECT clause to fetch related child rows (e.g., an Account plus its Opportunities).

Real-world example: An Account detail view that previews the top Opportunities per Account.

public with sharing class AccountWithOppsRepo {
    public static List<Account> topOppsPerAccount() {
        return [
            SELECT Id, Name,
                   (SELECT Id, Name, StageName, Amount
                    FROM Opportunities
                    WHERE IsClosed = false
                    ORDER BY Amount DESC
                    LIMIT 3)
            FROM Account
            WHERE Industry = 'Technology'
            ORDER BY Name
            LIMIT 100
        ];
    }
}

Why it’s good: You get Accounts and a ready-to-use child list for each—no query-in-a-loop surprises.



Parent-to-Child Relationship Query (Subquery) & N+1 Solution



Aggregate SOQL: summarize with GROUP BY / HAVING

Concept: Let the database do the math with COUNT, SUM, MIN, MAX, AVG, GROUP BY, and HAVING.

Real-world example: Show open-pipeline totals per Account and filter to accounts that actually matter.

public with sharing class PipelineSummary {
    public class Row { public Id accountId; public Decimal total; public Integer deals; }

    public static Map<Id, Row> openByAccount() {
        Map<Id, Row> out = new Map<Id, Row>();
        for (AggregateResult ar : [
            SELECT AccountId a, SUM(Amount) s, COUNT(Id) c
            FROM Opportunity
            WHERE IsClosed = false
            GROUP BY AccountId
            HAVING SUM(Amount) > 10000
        ]) {
            Row r = new Row();
            r.accountId = (Id) ar.get('a');
            r.total     = (Decimal) ar.get('s');
            r.deals     = (Integer) ar.get('c');
            out.put(r.accountId, r);
        }
        return out;
    }
}

Why it’s good: One query delivers your rollups—no manual counters or per-account loops.


Semi-joins & anti-joins: filter sets efficiently

Concept: Use WHERE Id IN (SELECT …) or NOT IN (SELECT …) to filter by related records directly in SOQL—no Apex side joins.

Real-world example: Find Accounts with no open Opportunities for re-engagement campaigns.

public with sharing class AccountGaps {
    public static List<Account> withoutOpenOpps() {
        return [
            SELECT Id, Name, OwnerId
            FROM Account
            WHERE Id NOT IN (
                SELECT AccountId FROM Opportunity WHERE IsClosed = false
            )
            AND IsDeleted = false
            LIMIT 500
        ];
    }
}

 

The N+1 Problem: a common performance trap ?

N+1 happens when you query a list (the “1”) and then, inside a loop, run another query per record (the “+N”). That burns through SOQL limits and drags performance.

Bad (N+1): One query for Accounts, then a query for Contacts inside the loop.
Good (single query): Get Accounts with their Contacts via a subquery.

❌ Avoid this (N+1):

// 1 query to get the Accounts
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Industry = 'Media'];

// N more queries inside the loop! (Very bad)
for (Account acc : accounts) {
    // This query runs for every single account, hitting the 100 SOQL limit quickly.
    List<Contact> contacts = [SELECT Id, Name FROM Contact WHERE AccountId = :acc.Id];
    // ... process contacts
}


✅ Do this instead (one query):

// Only 1 query to get everything!
List<Account> accountsWithContacts = [
    SELECT Name, (SELECT Id, Name FROM Contacts)
    FROM Account
    WHERE Industry = 'Media'
];

// No queries inside the loop
for (Account acc : accountsWithContacts) {
    // The contacts are already in memory, attached to the account
    List<Contact> contacts = acc.Contacts;
    // ... process contacts efficiently
}

Concept recap to avoid N+1:

  • Collect needed IDs into a Set<Id>.

  • Query once with WHERE Id IN :ids (or use relationship fields/subqueries).

  • Put results in a Map and enrich in memory.

Real-world example: A trigger needs each Case’s parent Account Tier__c—without querying per Case.

public with sharing class CaseEnricher {
    public static void beforeInsertOrUpdate(List<Case> cases) {
        // 1) Collect parent AccountIds from Cases (via Contact or Account lookup)
        Set<Id> accountIds = new Set<Id>();
        for (Case c : cases) {
            if (c.AccountId != null) accountIds.add(c.AccountId);
        }

        if (accountIds.isEmpty()) return;

        // 2) Single query for needed fields
        Map<Id, Account> acctById = new Map<Id, Account>([
            SELECT Id, Name, Tier__c
            FROM Account
            WHERE Id IN :accountIds
        ]);

        // 3) Enrich cases using the map
        for (Case c : cases) {
            Account a = acctById.get(c.AccountId);
            if (a != null) c.SLA_Tier__c = a.Tier__c;
        }
    }
}

 

Realtionship Model

Why it’s good: One query serves the entire batch—bulk-safe, predictable, and fast.


Combine subqueries + aggregates: dashboard-ready data

Concept: Use aggregates for ranking/counts, then a parent→child subquery for on-screen details.

Real-world example: A dashboard card: “Top 10 Accounts by open pipeline” + the three biggest opps per Account.

public with sharing class TopAccountsService {
    public class AccountCard {
        public Account acct;
        public Decimal openTotal;
        public Integer openCount;
        public List<Opportunity> topOpps;
    }

    public static List<AccountCard> build() {
        // A) Aggregate totals first
        List<AggregateResult> ar = [
            SELECT AccountId a, SUM(Amount) s, COUNT(Id) c
            FROM Opportunity
            WHERE IsClosed = false
            GROUP BY AccountId
            ORDER BY SUM(Amount) DESC
            LIMIT 10
        ];
        Set<Id> acctIds = new Set<Id>();
        Map<Id, AccountCard> cards = new Map<Id, AccountCard>();
        for (AggregateResult r : ar) {
            Id aid = (Id) r.get('a');
            AccountCard card = new AccountCard();
            card.openTotal = (Decimal) r.get('s');
            card.openCount = (Integer) r.get('c');
            cards.put(aid, card);
            acctIds.add(aid);
        }

        // B) Pull account basics + top 3 opps via subquery
        for (Account a : [
            SELECT Id, Name, Industry,
                   (SELECT Id, Name, Amount, StageName
                    FROM Opportunities
                    WHERE IsClosed = false
                    ORDER BY Amount DESC
                    LIMIT 3)
            FROM Account
            WHERE Id IN :acctIds
        ]) {
            AccountCard card = cards.get(a.Id);
            card.acct = a;
            card.topOpps = a.Opportunities; // child collection from subquery
        }

        return new List<AccountCard>(cards.values());
    }
}

Why it’s good: Two queries power a rich card: one to rank, one to show detail—still bulk-safe.


Short summary

  • Use relationship SOQL to grab parent fields in one shot.

  • Use parent→child subqueries to bring back related lists.

  • Use aggregate SOQL for counts/totals on the server.

  • Prefer semi/anti-joins for set-based filtering.

  • Always avoid N+1: batch IDs, query once, and map in memory.

These patterns cut query counts, reduce governor pressure, and keep your Apex clean and fast.

  • October 18, 2025