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
| Limit | Value |
|---|---|
| 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 chunk | Counts 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.LIMITwithout 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
executewith 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.