How to Count All Attachments and ContentDocuments in Your Salesforce Org
Every Salesforce file migration starts with the same question: how many files are we dealing with? The answer drives scope, timeline, cost, and tool choice. But Salesforce makes counting harder than it should be — files live across four different objects, and a naive query can double-count or miss them entirely. This guide gives you the SOQL you actually need, explains what each query is counting (and why), and walks through a worked example for scoping a real migration.
Why count files before you migrate?
Three reasons. First, scope: a 5,000-file migration is a one-day project and a 500,000-file migration is a six-week project. Second, cost: Salesforce file storage is priced per GB, and the bigger the file footprint the bigger the bill — both at the source (where you may be paying for storage you'll abandon) and at the target (where you need capacity to receive it). Third, tooling: under 1,000 files you can probably do it manually; above that you need real tooling with progress tracking, retry logic, and per-file reporting.
Counting also surfaces surprises. The legacy Attachment count is usually a multiple of what people assume, because Attachments aren't visible in the modern Files UI but accumulate quietly on records.
The three file types in Salesforce
Files in Salesforce live in three places:
- Modern Files —
ContentDocument+ContentVersion+ContentDocumentLink. The current model. Visible in the "Files" related list, in Files Home, and in the Notes & Attachments list (yes, both). - Legacy Attachments —
Attachmentobjects. Pre-2016 model. Visible only in the "Notes & Attachments" related list. Many orgs have lots of these and don't realize it. - Notes — both legacy
Noteobjects and modernContentNoterecords. Notes contain text content but they're stored alongside files in some object models, so a thorough inventory includes them.
Migration tools handle each type differently. The tooling section of our ContentVersion vs. Attachment guide covers the differences in detail.
Counting ContentDocuments and ContentVersions
The simplest count — total ContentDocuments in the org:
SELECT COUNT() FROM ContentDocument
That gives you the number of distinct files. Each ContentDocument has at least one ContentVersion, but may have many (one per uploaded version). To count just the latest version of each file:
SELECT COUNT() FROM ContentVersion WHERE IsLatest = true
That number should match the ContentDocument count. If it doesn't, you have either deleted ContentVersions or — more commonly — a few ContentDocuments with no live versions due to an admin's hard-delete operation.
To count all versions including history:
SELECT COUNT() FROM ContentVersion
The difference between the two ContentVersion counts tells you the version-history overhead in your storage.
Counting legacy Attachments
The Attachment object is dead simple — one row per file:
SELECT COUNT() FROM Attachment
To filter by parent object type, use the ParentId prefix. Account IDs start with 001, Contacts with 003, Cases with 500, Opportunities with 006:
SELECT COUNT() FROM Attachment WHERE Parent.Type = 'Account'
Or to get a parent-type breakdown in one query:
SELECT Parent.Type, COUNT(Id)
FROM Attachment
GROUP BY Parent.Type
ORDER BY COUNT(Id) DESC
Counting Notes
Both objects, separately:
SELECT COUNT() FROM Note
SELECT COUNT() FROM ContentNote
The legacy Note object is the older model; ContentNote is the modern one (and is itself a flavor of ContentDocument, so it'll already be counted in your ContentDocument total above). For a true unique count, query Note separately and add it to the ContentDocument count.
Per-object breakdowns
ContentDocumentLink is the join table connecting ContentDocuments to parent records. To count modern files per parent object:
SELECT LinkedEntity.Type, COUNT(Id)
FROM ContentDocumentLink
GROUP BY LinkedEntity.Type
ORDER BY COUNT(Id) DESC
Note that ContentDocumentLink can have multiple rows per ContentDocument (a file shared with three Accounts has three ContentDocumentLink rows). The count above is link count, not unique file count. To dedupe, count ContentDocumentId distinct values — but SOQL doesn't support COUNT_DISTINCT on standard objects. The workaround is to query ContentDocumentLink grouped by ContentDocumentId and count the result rows externally.
For a more complete parent-object breakdown, the SF Count Attachments tool handles dedup automatically — and includes custom objects without needing you to know their key prefixes in advance.
Counting by file size
Counts alone don't tell you what your storage looks like. To total file size on modern files:
SELECT SUM(ContentSize) FROM ContentVersion WHERE IsLatest = true
That returns total bytes. Divide by 1024 * 1024 * 1024 for GB.
For Attachments:
SELECT SUM(BodyLength) FROM Attachment
Together those two numbers tell you your total file storage footprint — useful for both migration planning and storage cost calculation. See our storage cost reduction guide for what to do with that number.
Worked example: scoping a 50K-file org
Imagine you run the queries above and find:
| Type | Count | Total size |
|---|---|---|
| ContentDocument | 32,400 | 78 GB |
| Attachment | 17,200 | 22 GB |
| Note (legacy) | 8,100 | 12 MB |
| Total files | 49,600 | 100 GB |
And the per-object breakdown:
| Parent object | ContentDocuments | Attachments |
|---|---|---|
| Case | 14,800 | 9,200 |
| Account | 8,600 | 4,400 |
| Opportunity | 5,200 | 2,100 |
| Custom_Project__c | 3,800 | 1,500 |
Several scoping decisions fall out:
- The Case-related files alone are 24,000 — the migration's largest single object scope. Plan it as a phase by itself.
- Legacy
Attachments are 35% of files and 22% of storage. Decide up front: migrate them asAttachments in the target, convert them toContentDocuments in flight, or leave them behind. - 100 GB of storage is non-trivial. Verify the target org's file storage limit (typically 10 GB included plus per-license additions). May require purchased storage or pre-migration archival.
- The custom object
Custom_Project__chas 5,300 files. Confirm it exists in the target org with matching field schema before migration starts.
Gotchas and edge cases
- Soft-deleted files —
ContentDocumentrecords in the Recycle Bin still exist but are excluded from default queries. UseALL ROWSonly if you specifically want them. - Files attached to deleted parents —
ContentDocumentLinkrows can outlive their parent. Filter onLinkedEntityIdvalidity if cleanliness matters. - Email attachments — outbound and inbound emails store attachments differently (sometimes as
EmailMessage.Attachments, sometimes asAttachment, sometimes asContentDocument). Worth a separate count if your org uses Email-to-Case heavily. - Sandbox vs production drift — Partial Copy sandboxes don't include all files. Don't count in sandbox and assume the number applies to production.
Need a precise inventory without writing all this SOQL?
The SF Count Attachments tool runs the queries above (and several you didn't know you needed) and exports a CSV report broken down by object type and file category — including custom objects with no manual configuration.