To sort Salesforce SOQL results by a custom field in ascending order with blank values at the bottom, write ORDER BY CustomField__c ASC NULLS LAST. SOQL puts nulls first on an ascending sort by default, so you have to say NULLS LAST to move them. The same syntax works on parent lookup fields through __r notation, and you can chain up to 32 sort keys in one query.
SOQL ORDER BY: the basic syntax
The default direction is ascending, the default null placement depends on direction:
// Ascending (default), NULLS FIRST by default
SELECT Id, Name, Amount FROM Opportunity ORDER BY Amount
// Descending, NULLS LAST by default
SELECT Id, Name, Amount FROM Opportunity ORDER BY Amount DESC
// Explicit null placement
SELECT Id, Name, CloseDate FROM Opportunity ORDER BY CloseDate ASC NULLS LAST
Three things worth holding on to. ASC is implicit, so ORDER BY Amount and ORDER BY Amount ASC are the same query. The null default flips with direction: ascending puts nulls first, descending puts them last, and you override only when the placement carries meaning. And direction applies per column, which is the one that catches people out. ORDER BY A DESC, B sorts A descending and B ascending; the DESC does not carry over.
Multi-column sorting (up to 32 keys)
When you need a tiebreaker, comma-separate columns. Each gets its own optional ASC/DESC:
// Group by Industry first, then by largest deal within each group
SELECT Id, Name, Industry, Amount
FROM Account
ORDER BY Industry ASC, Amount DESC
The first column is the primary sort key; the ones after it only matter when the earlier columns tie. SOQL allows up to 32 sort keys per query, but in real code you rarely need more than three. If you find yourself reaching for a fourth, the data probably wants reshaping in Apex instead.
Sorting by parent (lookup) fields
You can sort by any field on a related parent object using the relationship name:
// Contacts sorted by their Account's industry, then by Account name
SELECT Id, FirstName, LastName, Account.Industry, Account.Name
FROM Contact
ORDER BY Account.Industry, Account.Name
Note the relationship name, Account and not AccountId. You are sorting on a field of the related record, not on the foreign key column itself. For custom relationships, swap the suffix: Project__r.Status__c (use __r, not __c).
NULLS FIRST vs NULLS LAST in practice
Use NULLS FIRST/LAST whenever the column is optional and the null position changes meaning. Two examples that come up constantly:
// Show pending opportunities (no close date set) at the top
SELECT Id, Name, CloseDate
FROM Opportunity
WHERE StageName = 'Negotiation/Review'
ORDER BY CloseDate ASC NULLS FIRST
// Show contacts who *have* a recent activity ahead of those who don't
SELECT Id, Name, LastActivityDate
FROM Contact
ORDER BY LastActivityDate DESC NULLS LAST
That second case, real activity at the top and everything untouched at the bottom, is why I override the default more than any other.
Performance: when ORDER BY is fast vs slow
ORDER BY is cheap when it lines up with an index and expensive when it doesn't. Salesforce auto-indexes:
Id,Name,OwnerId,CreatedDate,LastModifiedDate,SystemModstamp- All foreign-key (lookup/master-detail) fields
- Custom fields marked
External IDorUnique - Standard fields with high selectivity (varies by org)
Sort on something outside that list, a custom Status picklist say, and the optimizer falls back to a full table scan. At large data volumes that means a timeout. Two ways out: shrink the result first with a selective WHERE clause, or have an admin request a custom index through Salesforce Support.
There is also a 32,000-row hard limit on sorted results in synchronous Apex. Past that, the query throws SOQL_OFFSET_TOO_LARGE if you also use OFFSET, or it simply truncates. For batches over 32k rows, switch to a query with Apex cursors or Database.QueryLocator in a Batch class.
ORDER BY in aggregate queries
When you GROUP BY, the ORDER BY columns have to be either in the GROUP BY clause or aggregated:
// Top 10 industries by deal count
SELECT Industry, COUNT(Id) totalCount
FROM Account
GROUP BY Industry
ORDER BY COUNT(Id) DESC
LIMIT 10
The aggregate alias works too: ORDER BY totalCount DESC does the same thing. ORDER BY a column you did not group on and you get MALFORMED_QUERY: field 'X' must be in GROUP BY clause.
Quick reference cheat sheet
| Goal | Pattern |
|---|---|
| Default ascending | ORDER BY field |
| Descending | ORDER BY field DESC |
| Multi-column | ORDER BY a, b DESC, c |
| Show nulls first/last | ORDER BY field ASC NULLS LAST |
| Sort by parent field | ORDER BY Account.Name |
| Sort aggregate | ORDER BY COUNT(Id) DESC |
| Pagination friendly | ORDER BY Id (Id is always indexed) |
Common mistakes
- Forgetting the tiebreaker.
ORDER BY CreatedDate DESCreturns rows in some order when a batch of records shares the same timestamp. AddIdas a secondary sort for deterministic pagination. - Sorting on a formula field. Formula fields are not stored, they are calculated at query time, so
ORDER BY MyFormula__cforces an evaluation per row. If you sort on one often, write the value into a regular field with a Flow. - Mixing up relationship and field names. It is
Account.Name(relationship.field), neverAccountId.Name. If you are unsure,sf data queryin the Salesforce CLI shows the right names interactively.
None of this is hard, but it is easy to half-learn and get bitten six months later. Pick an indexed sort column, set the null placement on purpose, add a tiebreaker. That covers most of the sort bugs I have watched reach production.
Leave a Comment