soql-analyzer
Skill1 starsChanged 7 months agoMITView on GitHub
Static-analyze SOQL queries in Apex source for selectivity issues — non-selective WHERE clauses, missing indexed fields, full-table scans on LDV objects, missing LIMIT. Cross-references the org cache (when present) for actual indexed fields and row volumes.
In this repository53 other files it gives its agents.
- templates/CLAUDE.mdCLAUDE.md
- adrSkill
- agent-deploySkill
- agent-discoverSkill
- agent-eval-trendSkill
- agent-exchange-listSkill
- agent-specSkill
- agent-testSkill
- agent-vs-flow-vs-apexSkill
- before-vs-after-triggerSkill
- code-reviewSkill
- complexitySkill
- coverage-trendSkill
- dead-codeSkill
- dependency-graphSkill
- deploySkill
- destructive-changesSkill
- diff-deploySkill
- erdSkill
- field-impactSkill
- flaky-test-finderSkill
- flow-auditSkill
- flow-vs-apexSkill
- fls-auditSkill
- gateway-configSkill
- generate-docsSkill
- limit-usageSkill
- lwc-vs-reactSkill
- mcp-tool-vs-restSkill
- notifySkill
- onboardSkill
- org-diffSkill
- org-exploreSkill
- package-versionSkill
- pattern-packSkill
- perf-reviewSkill
- permset-auditSkill
- pr-prepareSkill
- queueable-vs-batchSkill
- quick-deploySkill
- react-initSkill
- release-notesSkill
- scratch-orgSkill
- security-scanSkill
- sequence-diagramSkill
- sf-initSkill
- sharing-reviewSkill
- slack-agentSkill
- test-coverageSkill
- test-dataSkill
- test-planSkill
- trust-evalSkill
- trust-layer-auditSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 79
Look up in the org cache: actual indexed fields (custom-indexed flag), row count if available
- Very specific: line 131
Honor the org cache: If ${CACHE} lists a custom index on Status__c, don't flag WHERE Status__c = as non-selective.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
You are analyzing SOQL queries for selectivity. A selective query uses an indexed field with a value that returns a small fraction of total rows; non-selective queries scan large tables and trigger query timeouts on LDV (Large Data Volume) objects. This skill flags candidate problems before they become production incidents.
Read Project Config First
source "${CLAUDE_PLUGIN_ROOT}/hooks/lib/config.sh"
APEX_SRC="$(sf_config_get '.paths.apexSource' "$ENV")"
ORG="$(sf_config_get '.platform.defaultTargetOrg' "$ENV")"
CACHE="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugin-data}/argo/org-cache/${ORG}.json"Indexed Fields (Salesforce-managed by default)
These fields are always indexed unless the query disables the index:
Id- Foreign-key (lookup / master-detail) fields
Nameon most standard objectsRecordTypeIdOwnerIdCreatedDate,LastModifiedDate,SystemModstampExternal IdandUniquecustom fields- Custom fields explicitly indexed (custom indexes — visible only via the org cache, not source)
Non-indexed by default:
- Other custom fields (
Status__c,Region__c, etc.) Description,Body, long text areas
Selectivity Rules
| Filter pattern | Selective? |
Id = :var or Id IN :ids | ✅ always |
<Lookup>__c = :var | ✅ if cardinality is reasonable |
<Indexed> = :var | ✅ |
<NonIndexed> = :var (small object, < 10K rows) | ✅ acceptable |
<NonIndexed> = :var (LDV object, > 1M rows) | ❌ non-selective; needs index or different approach |
LIKE '%value' (leading wildcard) | ❌ never uses index |
LIKE 'value%' (trailing wildcard) | ✅ uses index if field is indexed |
NOT IN, != | ❌ generally non-selective |
<NULL> = NULL | ❌ NULL doesn't index |
Multi-OR across non-indexed fields | ❌ |
Input
$ARGUMENTS:
- (empty) — analyze all
.clsand.triggerfiles <ClassName>— analyze a single class--ldv-threshold <rows>— flag any object with > N rows in the org cache as LDV (default1000000)--target-org <alias>/--env <name>— for live cardinality / index lookup--ci/--format json|sarif/--out <path>
Steps
1. Walk the Apex source
For each file, find SOQL statements:
\[\s*SELECT\s+(.+?)\s+FROM\s+(\w+)(?:\s+(?:USING\s+SCOPE\s+\w+)?\s*WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(.+?))?(?:\s+LIMIT\s+(\d+))?\s*\]Multi-line / dynamic SOQL via Database.query: capture the string literal portion; if dynamic, flag for manual review.
2. Parse each WHERE clause
For each predicate:
- Identify the field
- Identify the operator
- Identify the right-hand value (literal, bind variable, dynamic)
3. Classify the query
For the FROM object:
- Look up in the org cache: actual indexed fields (custom-indexed flag), row count if available
- Map predicates against the indexed/non-indexed table above
Findings:
| Issue | Severity | Rule ID |
| Query has no WHERE clause on a custom or sensitive standard object | error | SOQL-NO-WHERE |
| WHERE filters on only non-indexed fields, FROM is custom or LDV-flagged object | warning | SOQL-NON-SELECTIVE |
Leading % in LIKE | warning | SOQL-LIKE-LEADING-WILDCARD |
NOT IN or != as primary filter | warning | SOQL-NEGATIVE-FILTER |
No LIMIT and FROM is LDV | warning | SOQL-NO-LIMIT-LDV |
ORDER BY on non-indexed field with no LIMIT | warning | SOQL-ORDER-BY-NON-INDEXED |
| Dynamic SOQL — string concatenation builds WHERE | note | SOQL-DYNAMIC (pair with /security-scan for SOQL injection check) |
4. Output
Default Markdown:
# SOQL Analysis: <project.name>
Apex files scanned: 47 | Queries found: 124
LDV threshold: 1,000,000 rows
Org cache: <CLAUDE_PLUGIN_DATA>/.../DevVM.json (cachedAt: 2026-04-28T12:30:00Z)
## Findings
### Critical (3)
- `force-app/.../OrderReportController.cls:42` — `[SELECT ... FROM Order__c]` (no WHERE) — Order__c has 2.4M rows
- ...
### High (5)
- `force-app/.../SearchService.cls:18` — `[SELECT Id FROM Account WHERE Description LIKE '%...']` — leading wildcard, full scan
- `force-app/.../UnusedJob.cls:55` — `[SELECT ... FROM Lead]` (no LIMIT) on LDV
- ...
### Medium (8)
- `force-app/.../StatusBatch.cls:30` — `[SELECT ... FROM Order__c WHERE Status__c = :s]` — Status__c not indexed; consider custom index or external archive
### Notes (2)
- `force-app/.../DynamicReportController.cls:88` — Dynamic SOQL (Database.query) — manual review neededCI mode: SARIF emit per finding.
5. Exit codes
- 0 — no error/warning findings
- 1 — any finding
- 2 — config error / no Apex source
Rules
- Heuristic, not perfect. Apex parsing without an AST is approximate. Mark dynamic SOQL as a note and let the human inspect
- Honor the org cache. If
${CACHE}lists a custom index onStatus__c, don't flagWHERE Status__c =as non-selective. Run/argo:org-explore --refreshto update - Don't flag tests.
*<testSuffix>.clsis excluded by default - Cross-link to fixes. Each finding's message should mention the appropriate fix: "add a custom index" / "filter by Id or lookup field" / "consider archiving" / "convert to a paginated report"
Consumers
/argo:code-reviewconsumes the JSON output and rolls up into the review report@architectand@data-architectreference the report when designing new query paths
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

