SOQL Object Limits and Restrictions
This skill activates when a SOQL query fails — or has to be written defensively — because of a limit that belongs to the object it targets, not to the generic governor limits. Salesforce documents that "SOQL applies specific limits to objects and situations," and those per-object rules (hard row caps, mandatory filter fields, required LIMIT clauses, indexed-field-only filtering) sit on top of the normal query limits and fail queries that would otherwise be perfectly legal.
Before Starting
Gather this context before working on anything in this domain:
- Identify the target object. The restriction is keyed to the object in the
FROM clause (and sometimes the field in SELECT). A query on Account has none of these; a query on Attachment, ContentDocumentLink, UserRecordAccess, a big object, or an external object may fail on a rule the object alone imposes.
- Know the running user's permissions. Several limits lift only when the user holds View All Data (the Attachment 100,000 cap, the
LIMIT requirement on TopicAssignment / NewsFeed / UserProfileFeed). Whether your code runs in user mode or system mode changes what actually happens.
- Separate these from the generic governor limits. A query can be well inside the 100-queries / 50,000-rows-retrieved Apex governor limits and still fail on a per-object rule. Don't debug a per-object failure by chasing governor limits.
- These are unversioned reference limits. The docs do not stamp them GA/Beta, and the exact numbers can change between releases. Treat the values here as current-as-documented and re-check the SOQL and SOSL Reference before relying on an edge case.
Core Concepts
The four shapes of a per-object restriction
Every restriction on the page reduces to one of four shapes. Knowing which shape you are facing tells you how to fix it:
- Hard row cap — the query fails or is capped past a fixed count.
Attachment fails past 100,000 records; UserRecordAccess returns at most 200 records no matter what you filter on.
- Mandatory filter field — the query is invalid unless it filters on a specific field.
ContentDocumentLink must filter on one of Id, ContentDocumentId, or LinkedEntityId; ContentHubItem on Id, ExternalId, or ContentHubRepositoryId; Vote on ParentId (single ID), Parent.Type (single type), or Id (single ID or list).
- Required
LIMIT absent a permission — the query must carry a LIMIT unless the user has View All Data. TopicAssignment needs LIMIT 1,100 or fewer; NewsFeed and UserProfileFeed need LIMIT 1,000 or fewer.
- Restricted filter surface — you cannot filter the object like a normal sObject. Big objects can filter only on their index fields, in index order, with no gaps; external objects cap joins and subqueries;
KnowledgeArticleVersion cannot take an Apex bind variable and must be queried with dynamic SOQL.
View All Data is the escape hatch — and the trap
Several caps and LIMIT requirements simply disappear when the running user holds View All Data. That makes it tempting to "fix" a failing query by granting the permission or moving the code to system mode. Resist it. View All Data is one of the broadest permissions in the platform; using it to make a query compile bypasses sharing for every object the transaction touches, not just the one that was failing. The correct fix is almost always to scope the query (add the filter, add the LIMIT) rather than to widen access.
Big objects and external objects filter differently
Big objects are not stored like standard/custom objects, so a big-object SOQL query can filter only on the fields in the object's index, and only in the order they appear in the index with no gaps. The last field in the filter may use =, <, >, <=, >=, or IN; every earlier field must use =. Operators !=, LIKE, NOT IN, EXCLUDES, and INCLUDES are unsupported. External objects live in a remote system reached over OData, so a single query allows up to 4 joins across external objects and other types, subqueries fetch up to 1,000 rows, and aggregate functions/clauses (AVG(), COUNT(fieldName), HAVING, GROUP BY, MAX(), MIN(), SUM()) are unsupported.
RecentlyViewed is a rolling window, not a table
RecentlyViewed rows are retained for 90 days and the object is periodically truncated down to about 200 records per object. It is a convenience view of recent activity, not a historical or audit dataset — never treat a RecentlyViewed query as a complete record of what a user has seen.
Common Patterns
Satisfy the mandatory filter (ContentDocumentLink / ContentHubItem / Vote)
When to use: any query against a file-linkage or vote object.
How it works: always include a WHERE clause on one of the object's allowed filter fields. For files attached to a record, filter ContentDocumentLink on LinkedEntityId; to find where a file is shared, filter on ContentDocumentId. Route the query through a selector so the constraint lives in one place.
Why not the alternative: a bare SELECT ... FROM ContentDocumentLink with no filter (or a filter on a different field) throws at runtime — there is no "return everything" mode for these objects.
Keep Attachment under the ceiling with a bounded WHERE + LIMIT
When to use: querying attachments across many parent records.
How it works: filter to a bounded parent set (e.g. WHERE ParentId IN :parentIds) and add a LIMIT that keeps the result under 100,000; for large content workloads, migrate off Attachment to ContentVersion / ContentDocument, which are the modern file model.
Why not the alternative: granting View All Data to lift the cap trades a bounded query problem for an org-wide sharing hole; adding OFFSET does not help because the query fails before paging matters.
Design the filter to match the big-object index
When to use: any query against a big object.
How it works: read the big object's index definition, then build the WHERE clause to cover the leading index fields with = and use a range operator (<, >, <=, >=, IN) only on the last field in the filter — never skip an index field. Avoid LIKE, !=, and NOT IN entirely.
Why not the alternative: treating a big object like a custom object (arbitrary fields, LIKE, out-of-order filters) produces a query that the platform rejects; there is no non-index path to the rows.
Decision Guidance
| Situation |
Recommended Approach |
Reason |
Query on ContentDocumentLink / ContentHubItem / Vote |
Add the mandatory WHERE on an allowed field |
The query is invalid without it — no unfiltered mode exists |
Attachment result set could exceed 100,000 |
Bounded WHERE + LIMIT, or migrate to ContentVersion |
Query fails past the cap; migration is the durable fix |
TopicAssignment / NewsFeed / UserProfileFeed query |
Add LIMIT (1,100 / 1,000 / 1,000) |
Required unless the user holds View All Data |
Need more than 200 UserRecordAccess rows |
Batch the IDs across multiple queries |
200 is a hard cap regardless of filter |
SELECT HasAccess ... FROM UserRecordAccess |
Add ORDER BY HasAccess |
The object requires it when HasAccess is selected |
| Query on a big object |
Filter index fields in order, = then one range op |
Big objects filter only on indexed fields, no gaps |
| Query joins several external objects |
Keep to ≤4 joins, subqueries ≤1,000 rows, no aggregates |
External-object query caps are enforced by the connector |
KnowledgeArticleVersion with a variable filter |
Build dynamic SOQL, not an inline bind |
Bind variables are not allowed on this object |
| A per-object limit is blocking you and View All Data would lift it |
Scope the query instead of granting the permission |
View All Data bypasses sharing org-wide, not just here |
Recommended Workflow
- Locate the target object. Read the
FROM clause (and any subquery FROMs). If it is a standard/custom object with none of these names, this skill does not apply — check governor limits or selectivity instead.
- Classify the restriction. Match the object to one of the four shapes: hard row cap, mandatory filter, required
LIMIT, or restricted filter surface. The object table in the work template lists each one.
- Rewrite the query to comply. Add the mandatory filter, add a bounded
LIMIT, batch to stay under a row cap, or reshape the WHERE to the big-object index — without reaching for View All Data to make it pass.
- Centralize it in the selector layer. Put the query in a selector that extends
templates/apex/BaseSelector.cls so it runs WITH USER_MODE and the per-object constraint is written once and unit-tested.
- Test at the boundary and without elevated permissions. Exercise the query with a dataset that approaches the cap, and — for permission-gated limits — run it as a user who does not hold View All Data (
System.runAs).
- Run the checker before deploy.
python3 scripts/check_soql_object_limits_and_restrictions.py --manifest-dir <path> flags the missing-filter, missing-LIMIT, and bind-variable red flags statically.
Review Checklist
Run through these before marking work in this area complete:
Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
- The Attachment cap is a hard failure, not a truncation — past 100,000 records the query errors out entirely; it does not silently return the first 100,000, and
OFFSET cannot page around it. Add a filter + LIMIT or migrate to ContentVersion.
- UserRecordAccess is capped at 200 and demands an ORDER BY — no filter raises the ceiling, and selecting
HasAccess without ORDER BY HasAccess is invalid. Batch your IDs and always order by HasAccess.
- Big-object filters fail on gaps and on the wrong operator — skipping an index field, or using
LIKE / != / NOT IN, produces a rejected query with no fallback path to the data.
Output Artifacts
| Artifact |
Description |
| Compliant SOQL query |
A query rewritten to satisfy the object's mandatory filter, LIMIT, row cap, or index constraint |
| Selector method |
The query centralized in a BaseSelector subclass, WITH USER_MODE, with the per-object rule encoded once |
| Checker report |
Output of check_soql_object_limits_and_restrictions.py listing missing filters, missing LIMITs, and bind-variable misuse |
| Remediation note |
Which restriction applied, the fix chosen, and why View All Data was not used |
Related Skills
apex/soql-fundamentals — base SOQL syntax and structure; start there if the question is about writing a query at all, not an object-specific limit.
apex/governor-limits and apex/apex-limits-monitoring — the generic per-transaction SOQL limits (query count, 50,000 rows retrieved) that these per-object rules sit on top of.
apex/apex-soql-relationship-queries — relationship-query depth (child/parent join levels), a different family of limits from the external-object join cap.
apex/apex-dynamic-soql-binding-safety — how to build the dynamic SOQL that KnowledgeArticleVersion requires without opening a SOQL-injection hole.
apex/apex-polymorphic-soql — filtering on Parent.Type, relevant to the Vote object's allowed filters.
data/external-data-and-big-objects and integration/salesforce-connect-external-objects — modelling big objects and external objects whose query surfaces this skill constrains.
data/soql-query-optimization — selectivity and index tuning once the query is legal but slow.
1---2name: soql-object-limits-and-restrictions3description: Use when a SOQL query fails or must be shaped around a specific object's own limits and restrictions — Attachment's 100,000-record ceiling, the mandatory filter fields on ContentDocumentLink / ContentHubItem / Vote, the required LIMIT clause on TopicAssignment / NewsFeed / UserProfileFeed (absent View All Data), the hard 200-row cap and ORDER BY HasAccess rule on UserRecordAccess, indexed-field-only filtering on big objects, the 4-join / 1,000-row-subquery caps on external objects, KnowledgeArticleVersion requiring dynamic SOQL, and RecentlyViewed's 90-day rolling window. NOT for generic SOQL governor limits — use apex/governor-limits. NOT for index and selectivity tuning — use data/soql-query-optimization. NOT for general SOQL syntax — use apex/soql-fundamentals.4---56# SOQL Object Limits and Restrictions78This skill activates when a SOQL query fails — or has to be written defensively — because of a limit that belongs to the **object it targets**, not to the generic governor limits. Salesforce documents that "SOQL applies specific limits to objects and situations," and those per-object rules (hard row caps, mandatory filter fields, required `LIMIT` clauses, indexed-field-only filtering) sit on top of the normal query limits and fail queries that would otherwise be perfectly legal.910---1112## Before Starting1314Gather this context before working on anything in this domain:1516- **Identify the target object.** The restriction is keyed to the object in the `FROM` clause (and sometimes the field in `SELECT`). A query on `Account` has none of these; a query on `Attachment`, `ContentDocumentLink`, `UserRecordAccess`, a big object, or an external object may fail on a rule the object alone imposes.17- **Know the running user's permissions.** Several limits lift only when the user holds **View All Data** (the Attachment 100,000 cap, the `LIMIT` requirement on `TopicAssignment` / `NewsFeed` / `UserProfileFeed`). Whether your code runs in user mode or system mode changes what actually happens.18- **Separate these from the generic governor limits.** A query can be well inside the 100-queries / 50,000-rows-retrieved Apex governor limits and *still* fail on a per-object rule. Don't debug a per-object failure by chasing governor limits.19- **These are unversioned reference limits.** The docs do not stamp them GA/Beta, and the exact numbers can change between releases. Treat the values here as current-as-documented and re-check the SOQL and SOSL Reference before relying on an edge case.2021---2223## Core Concepts2425### The four shapes of a per-object restriction2627Every restriction on the page reduces to one of four shapes. Knowing which shape you are facing tells you how to fix it:28291. **Hard row cap** — the query fails or is capped past a fixed count. `Attachment` fails past **100,000** records; `UserRecordAccess` returns at most **200** records no matter what you filter on.302. **Mandatory filter field** — the query is invalid unless it filters on a specific field. `ContentDocumentLink` must filter on one of `Id`, `ContentDocumentId`, or `LinkedEntityId`; `ContentHubItem` on `Id`, `ExternalId`, or `ContentHubRepositoryId`; `Vote` on `ParentId` (single ID), `Parent.Type` (single type), or `Id` (single ID or list).313. **Required `LIMIT` absent a permission** — the query must carry a `LIMIT` unless the user has View All Data. `TopicAssignment` needs `LIMIT 1,100` or fewer; `NewsFeed` and `UserProfileFeed` need `LIMIT 1,000` or fewer.324. **Restricted filter surface** — you cannot filter the object like a normal sObject. Big objects can filter **only** on their index fields, in index order, with no gaps; external objects cap joins and subqueries; `KnowledgeArticleVersion` cannot take an Apex bind variable and must be queried with dynamic SOQL.3334### View All Data is the escape hatch — and the trap3536Several caps and `LIMIT` requirements simply disappear when the running user holds View All Data. That makes it tempting to "fix" a failing query by granting the permission or moving the code to system mode. Resist it. View All Data is one of the broadest permissions in the platform; using it to make a query compile bypasses sharing for **every** object the transaction touches, not just the one that was failing. The correct fix is almost always to scope the query (add the filter, add the `LIMIT`) rather than to widen access.3738### Big objects and external objects filter differently3940Big objects are not stored like standard/custom objects, so a big-object SOQL query can filter **only on the fields in the object's index**, and only in the order they appear in the index with no gaps. The last field in the filter may use `=`, `<`, `>`, `<=`, `>=`, or `IN`; every earlier field must use `=`. Operators `!=`, `LIKE`, `NOT IN`, `EXCLUDES`, and `INCLUDES` are unsupported. External objects live in a remote system reached over OData, so a single query allows **up to 4 joins** across external objects and other types, subqueries fetch **up to 1,000 rows**, and aggregate functions/clauses (`AVG()`, `COUNT(fieldName)`, `HAVING`, `GROUP BY`, `MAX()`, `MIN()`, `SUM()`) are unsupported.4142### RecentlyViewed is a rolling window, not a table4344`RecentlyViewed` rows are retained for **90 days** and the object is periodically truncated down to about 200 records per object. It is a convenience view of recent activity, not a historical or audit dataset — never treat a `RecentlyViewed` query as a complete record of what a user has seen.4546---4748## Common Patterns4950### Satisfy the mandatory filter (ContentDocumentLink / ContentHubItem / Vote)5152**When to use:** any query against a file-linkage or vote object.5354**How it works:** always include a `WHERE` clause on one of the object's allowed filter fields. For files attached to a record, filter `ContentDocumentLink` on `LinkedEntityId`; to find where a file is shared, filter on `ContentDocumentId`. Route the query through a selector so the constraint lives in one place.5556**Why not the alternative:** a bare `SELECT ... FROM ContentDocumentLink` with no filter (or a filter on a different field) throws at runtime — there is no "return everything" mode for these objects.5758### Keep Attachment under the ceiling with a bounded WHERE + LIMIT5960**When to use:** querying attachments across many parent records.6162**How it works:** filter to a bounded parent set (e.g. `WHERE ParentId IN :parentIds`) and add a `LIMIT` that keeps the result under 100,000; for large content workloads, migrate off `Attachment` to `ContentVersion` / `ContentDocument`, which are the modern file model.6364**Why not the alternative:** granting View All Data to lift the cap trades a bounded query problem for an org-wide sharing hole; adding `OFFSET` does not help because the query fails before paging matters.6566### Design the filter to match the big-object index6768**When to use:** any query against a big object.6970**How it works:** read the big object's index definition, then build the `WHERE` clause to cover the leading index fields with `=` and use a range operator (`<`, `>`, `<=`, `>=`, `IN`) only on the last field in the filter — never skip an index field. Avoid `LIKE`, `!=`, and `NOT IN` entirely.7172**Why not the alternative:** treating a big object like a custom object (arbitrary fields, `LIKE`, out-of-order filters) produces a query that the platform rejects; there is no non-index path to the rows.7374---7576## Decision Guidance7778| Situation | Recommended Approach | Reason |79|---|---|---|80| Query on `ContentDocumentLink` / `ContentHubItem` / `Vote` | Add the mandatory `WHERE` on an allowed field | The query is invalid without it — no unfiltered mode exists |81| `Attachment` result set could exceed 100,000 | Bounded `WHERE` + `LIMIT`, or migrate to `ContentVersion` | Query fails past the cap; migration is the durable fix |82| `TopicAssignment` / `NewsFeed` / `UserProfileFeed` query | Add `LIMIT` (1,100 / 1,000 / 1,000) | Required unless the user holds View All Data |83| Need more than 200 `UserRecordAccess` rows | Batch the IDs across multiple queries | 200 is a hard cap regardless of filter |84| `SELECT HasAccess ... FROM UserRecordAccess` | Add `ORDER BY HasAccess` | The object requires it when `HasAccess` is selected |85| Query on a big object | Filter index fields in order, `=` then one range op | Big objects filter only on indexed fields, no gaps |86| Query joins several external objects | Keep to ≤4 joins, subqueries ≤1,000 rows, no aggregates | External-object query caps are enforced by the connector |87| `KnowledgeArticleVersion` with a variable filter | Build dynamic SOQL, not an inline bind | Bind variables are not allowed on this object |88| A per-object limit is blocking you and View All Data would lift it | Scope the query instead of granting the permission | View All Data bypasses sharing org-wide, not just here |8990---9192## Recommended Workflow93941. **Locate the target object.** Read the `FROM` clause (and any subquery `FROM`s). If it is a standard/custom object with none of these names, this skill does not apply — check governor limits or selectivity instead.952. **Classify the restriction.** Match the object to one of the four shapes: hard row cap, mandatory filter, required `LIMIT`, or restricted filter surface. The object table in the work template lists each one.963. **Rewrite the query to comply.** Add the mandatory filter, add a bounded `LIMIT`, batch to stay under a row cap, or reshape the `WHERE` to the big-object index — *without* reaching for View All Data to make it pass.974. **Centralize it in the selector layer.** Put the query in a selector that extends `templates/apex/BaseSelector.cls` so it runs `WITH USER_MODE` and the per-object constraint is written once and unit-tested.985. **Test at the boundary and without elevated permissions.** Exercise the query with a dataset that approaches the cap, and — for permission-gated limits — run it as a user who does **not** hold View All Data (`System.runAs`).996. **Run the checker before deploy.** `python3 scripts/check_soql_object_limits_and_restrictions.py --manifest-dir <path>` flags the missing-filter, missing-`LIMIT`, and bind-variable red flags statically.100101---102103## Review Checklist104105Run through these before marking work in this area complete:106107- [ ] Every `ContentDocumentLink` / `ContentHubItem` / `Vote` query filters on an allowed field108- [ ] `Attachment` queries are bounded (filter + `LIMIT`) or the workload moved to `ContentVersion`109- [ ] `TopicAssignment` / `NewsFeed` / `UserProfileFeed` queries carry a `LIMIT` (or the View All Data dependency is documented and justified)110- [ ] `UserRecordAccess` usage stays within 200 rows and adds `ORDER BY HasAccess` when `HasAccess` is selected111- [ ] Big-object filters use only index fields, in order, with `=` on all but the last field112- [ ] External-object queries keep ≤4 joins and ≤1,000-row subqueries and avoid aggregate functions113- [ ] `KnowledgeArticleVersion` filters use dynamic SOQL, not inline bind variables114- [ ] No per-object failure was "fixed" by granting View All Data or switching to system mode115116---117118## Salesforce-Specific Gotchas119120Non-obvious platform behaviors that cause real production problems:1211221. **The Attachment cap is a hard failure, not a truncation** — past 100,000 records the query errors out entirely; it does not silently return the first 100,000, and `OFFSET` cannot page around it. Add a filter + `LIMIT` or migrate to `ContentVersion`.1232. **UserRecordAccess is capped at 200 and demands an ORDER BY** — no filter raises the ceiling, and selecting `HasAccess` without `ORDER BY HasAccess` is invalid. Batch your IDs and always order by `HasAccess`.1243. **Big-object filters fail on gaps and on the wrong operator** — skipping an index field, or using `LIKE` / `!=` / `NOT IN`, produces a rejected query with no fallback path to the data.125126---127128## Output Artifacts129130| Artifact | Description |131|---|---|132| Compliant SOQL query | A query rewritten to satisfy the object's mandatory filter, `LIMIT`, row cap, or index constraint |133| Selector method | The query centralized in a `BaseSelector` subclass, `WITH USER_MODE`, with the per-object rule encoded once |134| Checker report | Output of `check_soql_object_limits_and_restrictions.py` listing missing filters, missing `LIMIT`s, and bind-variable misuse |135| Remediation note | Which restriction applied, the fix chosen, and why View All Data was not used |136137---138139## Related Skills140141- `apex/soql-fundamentals` — base SOQL syntax and structure; start there if the question is about writing a query at all, not an object-specific limit.142- `apex/governor-limits` and `apex/apex-limits-monitoring` — the generic per-transaction SOQL limits (query count, 50,000 rows retrieved) that these per-object rules sit on top of.143- `apex/apex-soql-relationship-queries` — relationship-query depth (child/parent join levels), a different family of limits from the external-object join cap.144- `apex/apex-dynamic-soql-binding-safety` — how to build the dynamic SOQL that `KnowledgeArticleVersion` requires without opening a SOQL-injection hole.145- `apex/apex-polymorphic-soql` — filtering on `Parent.Type`, relevant to the `Vote` object's allowed filters.146- `data/external-data-and-big-objects` and `integration/salesforce-connect-external-objects` — modelling big objects and external objects whose query surfaces this skill constrains.147- `data/soql-query-optimization` — selectivity and index tuning once the query is legal but slow.