System.QueryException: Non-selective query against large object type
A query in trigger context (or a similar latency-sensitive context) is missing an indexed filter on a high-volume object. The platform refuses non-selective queries on big tables to protect everyone. Add a filter on Id, Owner, or another indexed field.
Also seen asNon-selective query·non-selective query against large object·selective query trigger
On objects with more than ~200,000 rows, the platform requires queries to use a selective filter — a WHERE clause that narrows by an indexed field. Without one, the query forces a table scan, which on a large table takes minutes and grinds the org's database.
The platform refuses these queries with this error in trigger context, where milliseconds matter.
What counts as selective
A filter is selective if at least one filter clause uses an indexed field with the right operator:
| Indexed fields | Selective operators |
|---|---|
Id | =, IN, NOT IN |
Name | =, IN, LIKE 'foo%' (leading text) |
| External ID fields | =, IN |
| Foreign keys (lookup fields) | =, IN |
OwnerId | =, IN |
RecordTypeId | =, IN |
| Custom-indexed fields | depends on the index type |
!=, LIKE '%foo' (leading wildcard), and OR clauses with non-indexed fields are NOT selective — they force a full scan.
The shape of the bug
trigger AccountTrigger on Account (after update) {
// Non-selective on a 500k-Account org
List<Contact> kids = [SELECT Id FROM Contact WHERE Status__c = 'Active'];
}
Status__c (custom, non-indexed) is the only filter. On a big Contact table, this errors.
Fix 1: filter by an indexed field
Set<Id> accountIds = new Map<Id, Account>(Trigger.new).keySet();
List<Contact> kids = [
SELECT Id FROM Contact
WHERE AccountId IN :accountIds -- AccountId is indexed (FK)
AND Status__c = 'Active'
];
The platform picks the best available index to evaluate the query. With AccountId IN :small_set, it scans only Contacts under those Accounts — fast.
Fix 2: add a custom index
If you frequently filter by a non-indexed custom field, ask Salesforce Support to add an index. Setup → Object Manager → Field → Edit, and look for "External ID" or contact support to add a 1-tier index. Don't index every field — too many indexes slow down DML.
Fix 3: use a different storage strategy
If you're filtering by a status that's true for a small fraction of rows, denormalize:
- Add a Boolean field
Is_Active__c - Populate it when status is "Active"
- Index it (or use it as a filter combined with another indexed field)
This pattern works because indexes on Boolean fields with skewed distribution can be selective.
A subtle gotcha: IN with empty set
Set<Id> ids = new Set<Id>{}; // empty
List<Contact> kids = [SELECT Id FROM Contact WHERE AccountId IN :ids];
The platform may treat this as "no filter at all" and throw non-selective. Guard:
if (ids.isEmpty()) return new List<Contact>();
List<Contact> kids = [SELECT Id FROM Contact WHERE AccountId IN :ids];
When the query is selective but still slow
Selective != fast. A query selecting 100,000 rows under one indexed filter is selective but heavy. Use LIMIT to bound the result, or move to Batch Apex.
Diagnose with Query Plan
Setup → Developer Console → Query Editor → click the query → Query Plan. The plan shows which index the platform used, the cost, and the cardinality estimate. If the cost is >2 (out of a 0-1 ideal), your query is suboptimal. Add or change indexes accordingly.
