Database.QueryLocator is the standard return type from a batch start method. It represents the result of a SOQL query, but with a special trick: it can return up to 50 million records — bypassing the synchronous SOQL governor cap of 50,000 rows.
How you create one
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator(
'SELECT Id, Email FROM Contact WHERE Active__c = true'
);
}
You can pass either a SOQL string or a dynamic query built with bind variables:
String soql = 'SELECT Id, Status FROM Case WHERE Status = :statusFilter';
return Database.getQueryLocator(soql);
(The bind only works if statusFilter is in scope; the safer form is to use static SOQL where possible.)
Why use it over Iterable<sObject>?
| Capability | Database.QueryLocator | Iterable<sObject> |
|---|---|---|
| Max records | 50,000,000 | 50,000 |
| Source | Single SOQL query | Anything (in-memory list, calculated set) |
| Streaming | Yes (chunked by the platform) | All loaded at once |
| Performance | High | Limited by memory |
FOR UPDATE | No | N/A |
If a single SOQL query can express your dataset, always use QueryLocator. It’s the only path to truly large datasets.
How chunking works
Salesforce internally treats the QueryLocator like a cursor. When the batch runs:
startreturns the locator (the query is not fully executed yet).- The platform pages through results in chunks of your chosen size (default 200).
- Each chunk is passed to a fresh
executeinvocation in its own transaction.
You never see all 50 million records loaded into memory at once — the platform streams them.
Limits to know
| Limit | Value |
|---|---|
| Records returned by QueryLocator | 50,000,000 |
| Concurrent batch jobs | 5 (queued past that go to Flex Queue) |
| Chunk size range | 1–2,000 records |
| Default chunk size | 200 |
SOQL row count in start | Bypassed — QueryLocator is exempt |
What it can and can’t do
- Can use
ORDER BY,WHERE, joins, sub-selects (carefully). - Can be combined with
Database.Stateful. - Cannot use
FOR UPDATE(the chunk transactions don’t share a lock). - Cannot include aggregate queries (
COUNT(),GROUP BYetc.) — those returnAggregateResult, not sObjects.
When to use Iterable instead
Pick Iterable<sObject> when your dataset isn’t a single SOQL query — e.g., records computed from an external API or merged from multiple queries with deduping:
public Iterable<sObject> start(Database.BatchableContext ctx) {
// ... build custom list (≤ 50,000)
return customList;
}
You lose the 50M ceiling, but you gain flexibility.
Common interview follow-ups
- What does
Database.getQueryLocatorreturn when called outside a batch? — ADatabase.QueryLocatorobject, but useless without a batch context. Most apps never call it standalone. - Can the QueryLocator’s SOQL include sub-queries? — Yes, but the relationship sub-query has its own 200-row cap per parent record. Use cautiously.
- Does QueryLocator count against synchronous SOQL limits? — No — it’s the whole point.
Verified against: Apex Developer Guide — Database.QueryLocator Class. Last reviewed 2026-05-17.