If you've written more than a handful of SOQL queries, you know that ORDER BY looks deceptively simple — until you hit a tie, a null, or a relationship field. This guide walks through every pattern you'll actually use, including the sort-direction defaults, the NULLS FIRST/NULLS LAST semantics that trip up newcomers, and the indexing rules that decide whether your query runs in 50ms or 50 seconds.
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 to remember about the basics:
ASCis implicit.ORDER BY AmountandORDER BY Amount ASCare identical.- NULLS FIRST/LAST defaults flip with direction. Ascending puts nulls first; descending puts them last. Override only when you need to.
- Sort direction applies per column.
ORDER BY A DESC, Bsorts A descending and B ascending — theDESCdoesn't 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; subsequent columns only matter when the previous 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 not AccountId — because you're 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
The second case — "real activity first, no activity at the bottom" — is the most common reason to override the default.
Performance: when ORDER BY is fast vs slow
ORDER BY is cheap when it lines up with an index, 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)
If you're sorting on something not in that list — say, a custom Status picklist — the optimizer falls back to a full table scan. For large data volumes, that means timeout. Two mitigations: use a selective WHERE clause to shrink the result first, or have an admin add a custom index via Salesforce Support.
There's 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 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 must 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
You can also use the aggregate alias: ORDER BY totalCount DESC works the same. Don't try to ORDER BY a column you didn't group on — you'll 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 to break ties.
ORDER BY CreatedDate DESCreturns rows in some order when many records share the same timestamp. AddIdas a secondary sort for deterministic pagination. - Sorting on a formula field. Formula fields aren't stored — they're calculated at query time, so
ORDER BY MyFormula__cforces evaluation per row. For frequent sorts, materialize the value into a regular field via Flow. - Confusing relationship and field names. It's
Account.Name(relationship.field), neverAccountId.Name. Salesforce CLI'ssf data queryshows the right names interactively if you're unsure.
Sorting in SOQL is one of those features that rewards a few minutes of learning the rules upfront. Get the indexing right, choose your null placement deliberately, and chain your tiebreakers — and you'll never have a sort-related production support ticket again.
Leave a Comment