Skip to main content

SF-0376 · Scenario · Medium

Can we Query related records or child records using Database.QueryLocator?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

Yes. Database.QueryLocator supports the same SOQL syntax as any other query — including parent traversal (Account.Name) and child sub-queries ((SELECT Id FROM Contacts)). The 50M record ceiling applies to the top-level records; sub-queries return additional related rows per parent.

Parent traversal

public Database.QueryLocator start(Database.BatchableContext ctx) {
    return Database.getQueryLocator(
        'SELECT Id, Subject, AccountId, Account.Name, Account.Industry, Owner.Email ' +
        'FROM Case ' +
        'WHERE Status = \'New\''
    );
}

Each chunk receives Case records with Account.Name, Account.Industry, and Owner.Email populated through dot-notation. Standard parent traversal — up to 5 levels through relationships, like any SOQL.

Child sub-query

public Database.QueryLocator start(Database.BatchableContext ctx) {
    return Database.getQueryLocator(
        'SELECT Id, Name, ' +
        '       (SELECT Id, Status FROM Cases WHERE Status = \'New\') ' +
        'FROM Account WHERE Industry = \'Tech\''
    );
}

public void execute(Database.BatchableContext ctx, List<Account> scope) {
    for (Account a : scope) {
        for (Case c : a.Cases) { // sub-query result
            // ... process
        }
    }
}

The Cases relationship comes back as a.Cases inside execute. You can iterate without an extra SOQL call.

Sub-query limits to know

LimitValue
Top-level records (parent)Up to 50,000,000 (QueryLocator ceiling)
Related records per parent (from sub-query)200 by default, configurable up to a few hundred via LIMIT
Total related records across all parents in a chunkCounts toward the SOQL row limit (50,000) of execute

If a parent has 1,000 child cases, the sub-query in start returns the first 200. That’s a hard sub-query cap — not configurable beyond a couple hundred. For “all children” you usually re-query in execute:

public void execute(Database.BatchableContext ctx, List<Account> scope) {
    Set<Id> accountIds = new Map<Id, Account>(scope).keySet();
    Map<Id, List<Case>> caseMap = new Map<Id, List<Case>>();
    for (Case c : [SELECT Id, AccountId FROM Case WHERE AccountId IN :accountIds]) {
        if (!caseMap.containsKey(c.AccountId)) caseMap.put(c.AccountId, new List<Case>());
        caseMap.get(c.AccountId).add(c);
    }
    // process each Account with all its Cases
}

Aggregate queries

You cannot use GROUP BY, COUNT(), MAX(), etc., in a QueryLocator query — those return AggregateResult, not sObjects. Compute aggregates inside execute:

public void execute(Database.BatchableContext ctx, List<Account> scope) {
    AggregateResult[] results = [
        SELECT AccountId, COUNT(Id) cnt FROM Case
        WHERE AccountId IN :scope GROUP BY AccountId
    ];
    // ...
}

Avoid in QueryLocator

  • FOR UPDATE — not supported in batch.
  • OFFSET — works, but defeats the purpose of streaming.
  • LIMIT without need — usually unnecessary; the QueryLocator already streams.

Common interview follow-ups

  • Can I use polymorphic relationships (What.Type)? — Yes, same as any SOQL.
  • Can sub-queries trigger SOQL governor limits in execute? — Yes, every related row counts.
  • What if I need all children of every parent and there are thousands per parent? — Re-query inside execute with a chunked approach, or run a separate batch over the child object directly.

Verified against: Apex Developer Guide — Database.QueryLocator Class. Last reviewed 2026-05-17.