This is Part 2 of Salesforce things you should know. In Part 1 we covered Selective Queries and Indexes. Now let’s turn that theory into practice with the Query Plan. If you want predictable SOQL performance, this is the tool you open before you deploy your code.
What is the Query Plan
The Query Plan shows how Salesforce thinks about your SOQL. It estimates how many rows your filter will touch, which index could be used, and how expensive each option looks. Your job is to write filters that keep the best plan selective and cheap.
You will see multiple candidate plans(candidate plans are explained later in the blog). Pick the one with the lowest Cost and a small Cardinality that uses an indexed path.
Example:

Where to find Query Plan
Developer Console
- Open Developer Console.
- Go to Help > Preferences > check Enable Query Plan.
- Open Query Editor, paste your SOQL, click Query Plan.
Workbench
- Go to queries > SOQL Query.
- Paste your SOQL.
- Click Explain.
Either view gives you similar fields. The layout is different, the meaning is the same.
Columns you should care about in Query Plan
- sObject Cardinality
Estimated total rows in the object. Think of it as table size. - Cardinality
Estimated rows matched by your WHERE clause for that plan. Smaller is better. This is what drives selectivity. - Fields
The indexed field or fields a plan would use. Example:AccountId,CreatedDate,External_Id__c. If this is empty, the plan is probably a scan. - Leading Operation Type
The access path. Common values:- Index or a specific index name
- TableScan when no index is helpful
- CompositeIndex when a multi-column index exists
- Cost
A relative number. Lower is better. Do not try to convert it to milliseconds. Use it to compare plans for the same query. - Notes
Hints like “index does not satisfy order by,” “leading wildcard,” or “not selective.”
How to read a Query Plan quickly
- Scan the Leading Operation Type
If you see TableScan as the best option, your filter is not selective enough. Fix the filter or add a better index. - Compare Costs
Pick the plan with the smallest Cost that still makes sense. If two plans have similar cost, prefer the one that uses a stable, high-cardinality indexed field. - Validate Cardinality against thresholds
From Part 1:- Standard index: 30 percent of the first 1M rows and 15 percent of the rest, capped at 1M.
- Custom index: 10 percent of the first 1M and 5 percent of the rest, capped at 333,333.
- Check Fields
Make sure the plan is using the field you expect. If not, rewrite your filter so the optimizer can lead with that field.
Example 1 : turn a Query Plan into an index lookup
Before:
SELECT Id, Subject, Status
FROM Case
WHERE Status != 'Closed'
AND CreatedDate >= :System.today().addDays(-90)Why it is badStatus != 'Closed' is a negative filter. It matches most rows and is rarely selective. The plan will likely show a TableScan with a high Cost.
Fix:
SELECT Id, Subject, Status
FROM Case
WHERE IsClosed = false
AND CreatedDate >= :System.today().addDays(-30)
AND RecordTypeId = :supportRtId- Move to a boolean that is more selective.
- Tighten the CreatedDate window.
- Add RecordTypeId to narrow the working set.
- Check the plan again. You should see Leading Operation: Index on
CreatedDateorRecordTypeIdwith much lower Cardinality.
Example 2 : anchored LIKE vs leading wildcard
Unselective:
SELECT Id, Name
FROM Account
WHERE Name LIKE '%Acme%'The plan will call out leading wildcard. No index.
Selective:
SELECT Id, Name
FROM Account
WHERE Name LIKE 'Acme%'Anchored LIKE can use an index on Name if it exists or a custom index. Cardinality will drop.
If you truly need contains search across many fields, consider SOSL instead of SOQL.
Example 3 : too many rows

If you look at the query in above screenshot, you see two indexed fields, RecordType.name and RecordType.Id but you still see both tablescan and index are taking the same cost? Why? Short answer, your filter hits too many rows, so the index can’t help much.
Here’s what your Query Plan is telling you:
- sObject Cardinality = 179,402 -> total Opportunities.
- Cardinality = 102,708 -> the optimizer estimates your WHERE returns ~102.7k rows (that’s ~57% of the table).
Common plan warnings and what to do
- TableScan is cheapest
Your filter is not selective. Add a selective predicate on an indexed field, shrink date ranges, or split the query. - Index does not satisfy order by
If sorting is expensive, either remove theORDER BY, limit the scope, or ask for a composite index that matches the sort if it is critical. - Leading wildcard
Replace%term%withterm%, or move to SOSL. - Formula in filter
Formulas are not indexed. Persist the value into a real field and index that field. - OR across broad sets
The plan often degrades. Try splitting into multiple selective queries and union in Apex. Example:
List<Account> a = [
SELECT Id FROM Account WHERE Industry = 'Tech' AND BillingCountry = 'US'
];
a.addAll([
SELECT Id FROM Account WHERE Industry = 'Tech' AND BillingCountry = 'CA'
]);Practical workflow for developers
- Prototype with real literals
The Query Plan estimates based on your actual values. Use representative values, not placeholders. - Hit the selectivity thresholds
Use Part 1’s math to judge(Selective Queries and Indexes) if a plan will hold up in production data volumes. - Prefer equality and anchored text
=andINon indexed fields, anchoredLIKE, smallINlists. - Use audit dates to narrow scope
CreatedDateorLastModifiedDateranges are workhorses for selectivity. - Avoid negative and null filters in hot paths
!=,NOT IN,= NULLtend to be unselective. - Consider a custom index
When the filter is stable and deterministic, request an index on the field that actually drives selectivity. Remember custom index thresholds are stricter. - Refactor rather than force
If a plan stays unselective, change the model. Add a denormalized, indexed helper field or segment data by Record Type, Region, or Status.
Or use Skinny Table
For each object table that’s visible to you, Salesforce maintains other, separate tables at the database level for standard and custom fields. This separation, which is invisible to customers, ordinarily requires a join when a query contains both kinds of fields. A skinny table contains both kinds of fields
Salesforce can create skinny tables to contain frequently used fields and to avoid joins. This can improve the performance of certain read-only operations. Skinny tables are kept in sync with their source tables when the source tables are modified.

Conclusion
To wrap up, treat Query Plan like a pre-flight check for every serious SOQL. Paste your query, look for an index as the leading operation, compare Cardinality to the selectivity thresholds from Part 1 on Selective Queries and Indexes, and only ship when the lowest-cost plan is clearly selective. If it is not, tighten date windows, anchor text, add truly selective predicates, or ask for the right index. This habit turns “it works on my sandbox” into predictable performance in production.
In the next post I will cover Locking and Concurrency in Salesforce, including row locks, save order, and how FOR UPDATE changes the game.
