Large Data Volumes (LDV) is the point at which a Salesforce object grows big enough — usually a few million rows and up — that everyday queries, reports, and list views start to slow down. The fix is not a single switch but a toolkit: skinny tables (a narrow, read-only copy of your hottest fields), custom indexes (so the Query Optimizer can skip full table scans), selective SOQL, and cold-data stores such as Big Objects. Used together, these let one org scale into the tens or even hundreds of millions of records while keeping page loads and reports fast.
Key takeaways
- Skinny tables are read-only tables Salesforce Support builds for you that hold a subset of an object's frequently used fields, removing expensive joins. Max 100 columns, same-object fields only.
- You cannot create skinny tables yourself in production — you open a case with Salesforce Customer Support. They are also not copied during a sandbox refresh, so you must re-request them each time.
- Custom indexes make a
WHEREfilter selective so the Query Optimizer uses the index instead of scanning every row. - A filter is selective roughly when it returns < 10% of rows for a custom index (capped at ~333,333 rows) and < 30% of the first million rows for a standard index.
- For cold/archival data use Big Objects (billions of rows); for huge extracts use PK chunking via the Bulk API header
Sforce-Enable-PKChunking. - Watch for ownership skew and lookup skew — they slow sharing recalculation and record updates regardless of how well you index.
What are Large Data Volumes (LDV) in Salesforce?
LDV is not a fixed number you flip on; it is the threshold at which the sheer count of records changes how the platform behaves. As a rule of thumb, an object holding more than a few million rows — and certainly tens of millions — is in LDV territory. At that scale, a query that scans the whole table, a report with a wide date range, or a sharing recalculation that touches every row becomes expensive.
The symptoms are familiar: list views and reports time out, SOQL queries throw "non-selective query" errors, and data loads crawl. The cause is almost always the same — the database is reading far more rows than it needs to. Every technique below exists to reduce the number of rows touched or to avoid joins, so the work stays proportional to the data you actually want, not the data you have.
What is a skinny table and when should you use one?
A skinny table is a custom, read-only table that Salesforce maintains alongside a base object. It contains a chosen subset of that object's most-used fields (and the matching translation table), so reads against it avoid the join between the base table and its field-overflow tables. Because it is narrower and join-free, reports, list views, and SOQL that hit only those fields run noticeably faster.
A classic use case: instead of filtering a report on a date range like 01/01/2025 to 12/31/2025 — which forces a repeated, costly calculation for a yearly or year-to-date report — you add a Year field to the skinny table and filter on Year = '2025'. The platform decides at runtime whether to use the skinny table, so you do not rewrite reports or add any Apex.
Reach for a skinny table when an object has millions of rows and a stable set of fields is queried over and over in reports and list views. It is a read optimization, not a storage saving — the data still lives in the base object too.
How do skinny tables work? The facts that trip people up
These are the details that are most often misremembered, so treat them as hard constraints:
- Created and maintained only by Salesforce Customer Support. There is no self-service button in production — you log a case.
- Supported on Account, Contact, Lead, Opportunity, Case, and custom objects.
- Can hold a maximum of 100 columns.
- Cannot include fields from any other object — no cross-object joins.
- Kept automatically in sync with the base object by the platform; you maintain no triggers or sync code.
- Exclude soft-deleted records (rows in the Recycle Bin with
IsDeleted = true), which trims query output. - Not copied during a sandbox refresh. Support can copy a skinny table to a Full sandbox on request, but you must re-request it after every refresh.
- The Query Optimizer decides at query runtime whether to use the skinny table, so existing reports, list views, and SOQL need no changes.
- Supported field types include Checkbox, Date, Date/Time, Email, Number, Percent, Phone, Picklist, Multi-select Picklist, Text, Text Area, Long Text Area, and URL.
Because they are invisible to admins and tied to a support case, skinny tables work best on stable, high-volume objects — not on schemas you are still changing weekly.
What is a custom index and how does query selectivity work?
When you run a SOQL query, Salesforce's cost-based Query Optimizer estimates how many rows each filter will match. If a filter is selective — it narrows results to a small fraction of the table — the optimizer uses an index and jumps straight to those rows. If no filter is selective, it falls back to a full table scan, which is what produces slow queries and "non-selective query against large object type" errors.
Some fields are indexed automatically. The standard indexed fields include:
Id(record ID),Name,OwnerId,CreatedDate,SystemModstamp, andRecordTypeIdEmailon Contact and Lead- All foreign keys — master-detail and lookup relationship fields
- The Division field, and any field you mark Unique or External ID (these are auto-indexed without a support case)
If you frequently filter on a field that is not in that list, ask Salesforce to add a custom index on it. The thresholds the optimizer uses to decide selectivity are concrete:
| Index type | Selective when filter matches | Hard cap |
|---|---|---|
| Standard index | < 30% of the first 1M rows, < 15% of rows beyond 1M | 1,000,000 rows |
| Custom index | < 10% of total rows | 333,333 rows |
So on a 5-million-row object, a custom-indexed filter must return fewer than ~333,333 rows to stay selective. Filters that defeat indexes entirely include leading-wildcard LIKE '%term', negative operators (!=, NOT, NOT IN), comparisons on formula fields, and = null / != null checks on fields whose nulls are not indexed. Avoid those on large objects. For a refresher on writing the queries themselves, see how to run a SOQL query in Salesforce.
Which LDV technique should you reach for?
Skinny tables and indexes are only part of the picture. This table maps the common LDV tools to what they do and when to use them:
| Technique | What it does | When to use it |
|---|---|---|
| Skinny table | Read-only narrow copy of hot fields, no joins | Reports/list views on a multi-million-row object with a stable field set |
| Custom index | Makes a WHERE filter selective |
You filter often on a non-indexed field |
| Selective SOQL | Filters on indexed fields and adds LIMIT |
Every query against a large object |
| Divisions | Partitions records so queries hit a subset | Multi-region or multi-brand data in one org (enabled by Support) |
| PK chunking | Splits a Bulk query into Id ranges | Extracting tens of millions of rows |
| Defer sharing calculation | Suspends sharing recalculation during loads | Bulk inserts/updates of millions of records |
| Big Objects | Stores billions of rows of cold data | Audit logs, history, archives queried rarely |
| Archiving / truncation | Removes cold data from the hot object | Old records you almost never query |
PK chunking deserves a note: when extracting very large data sets through the Bulk API, add the HTTP header Sforce-Enable-PKChunking to a query job. Salesforce splits the work into chunks by record ID (default 100,000, max 250,000 rows per chunk) and runs them as parallel batches — far more reliable than one giant query that times out.
Big Objects are the right home for cold data. They store billions of records with horizontal scale, are queried with SOQL (synchronously on indexed fields) or asynchronously via Async SOQL, and keep your transactional objects small so the hot path stays fast.
How do you query LDV without hitting governor limits?
The single most important habit is to make every query selective. Filter on an indexed field, add a LIMIT, and avoid the index-defeating patterns above. On large objects, also prefer WHERE clauses that target a bounded slice of data rather than scanning all history:
-- Non-selective: scans the whole object, likely errors at LDV
SELECT Id, Name FROM Account WHERE Industry != null
-- Selective: indexed field + bounded range + LIMIT
SELECT Id, Name, AnnualRevenue
FROM Account
WHERE OwnerId = :userId
AND CreatedDate = LAST_N_DAYS:90
ORDER BY CreatedDate DESC
LIMIT 200
In Apex, this discipline also keeps you inside SOQL row and heap limits — batch large jobs with Batch Apex or the Bulk API instead of loading everything into one transaction. For the full set of platform ceilings to design around, read our guide to Salesforce governor limits, and pair it with these Salesforce customization best practices so your triggers and automations stay LDV-safe.
How do you request skinny tables and custom indexes?
Most LDV optimizations route through Salesforce Customer Support:
- Quantify the problem. Capture the object's row count, the slow report or query, and the fields filtered on. Run the Salesforce Optimizer report, which flags unindexed high-volume fields and other LDV risks.
- Open a support case requesting the skinny table (list the exact fields, ≤ 100) or the custom index (name the field). Salesforce reviews selectivity before enabling either.
- Auto-index what you can yourself: marking a field Unique or External ID creates an index without a case — a quick win for natural keys.
- Re-request after sandbox refreshes, since skinny tables do not carry over.
Getting these requests approved quickly is mostly about evidence. A documented, data-backed case is approved far faster than a vague "queries are slow" ticket — one reason teams lean on Salesforce managed services to handle the diagnosis and Support liaison.
What about ownership skew and lookup skew?
Two data-shape problems hurt LDV performance no matter how well you index:
- Ownership skew happens when more than ~10,000 records of one object are owned by a single user (often an integration or "default" user). Any role or sharing change for that user forces a massive recalculation. Spread ownership across users, or place the owner high in the role hierarchy with sharing kept simple.
- Lookup skew happens when a huge number of child records point to the same parent record. Concurrent updates then contend for a lock on that one parent row, causing
UNABLE_TO_LOCK_ROWerrors. Distribute references across more parent records to relieve the contention.
Designing the data model to avoid skew early is cheaper than retrofitting it. For broader process gains, see our techniques and tools to improve the Salesforce development cycle.
Scaling Salesforce to tens of millions of rows
There is no one fix for Large Data Volumes — you combine read optimizations (skinny tables, custom indexes, selective SOQL), partitioning (divisions, PK chunking), and cold-data offload (Big Objects, archiving), then keep the data model free of ownership and lookup skew. Get the diagnosis right and a single org comfortably serves hundreds of millions of records.
MicroPyramid has delivered Salesforce work for 12+ years (since 2014) across 50+ projects, including LDV performance tuning, indexing strategy, and large-scale data migrations. If your org is straining under its own data, see our Salesforce consulting services.
Frequently Asked Questions
Can I create a skinny table myself in Salesforce?
No. Skinny tables are created and maintained only by Salesforce Customer Support — there is no self-service option in a production org. You open a case listing the object and the (≤ 100) fields you want included, and Salesforce builds and auto-syncs the table for you.
How many columns can a skinny table have?
A skinny table can contain a maximum of 100 columns, and every column must come from the same base object — it cannot pull fields from related objects. Choose the fields that your high-volume reports and list views actually filter and display.
Do skinny tables survive a sandbox refresh?
No. Skinny tables are not copied automatically during a sandbox refresh. Salesforce Support can copy one to a Full sandbox on request, but you must re-request it after each refresh — plan for this in your release process.
What makes a SOQL filter "selective"?
A filter is selective when it matches a small enough fraction of rows for the Query Optimizer to use an index. The thresholds are roughly < 10% of total rows (capped at ~333,333) for a custom index and < 30% of the first million rows for a standard index. Leading-wildcard LIKE, negative operators, and null checks on unindexed fields are non-selective and force a full scan.
When should I use Big Objects instead of a skinny table?
Use a skinny table to speed up reads on active data you query constantly. Use a Big Object to offload cold data — audit trails, history, archives — that you query rarely but must retain. Big Objects scale to billions of rows and keep your transactional objects small, while skinny tables optimize the hot path.
At what record count do Large Data Volumes start to matter?
There is no hard line, but performance considerations typically begin around a few million records in an object and become critical in the tens of millions. The real trigger is behavior — slow reports, query timeouts, or "non-selective query" errors — rather than a single magic number.