Skip to main content
SFDC Developers
Apex

SOQL ORDER BY: Sort, NULLS FIRST/LAST & Multi-Column Examples

Vinay Vernekar · · 5 min read

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:

  1. ASC is implicit. ORDER BY Amount and ORDER BY Amount ASC are identical.
  2. NULLS FIRST/LAST defaults flip with direction. Ascending puts nulls first; descending puts them last. Override only when you need to.
  3. Sort direction applies per column. ORDER BY A DESC, B sorts A descending and B ascending — the DESC doesn'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 ID or Unique
  • 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 DESC returns rows in some order when many records share the same timestamp. Add Id as 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__c forces 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), never AccountId.Name. Salesforce CLI's sf data query shows 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.

Share this article

Get weekly Salesforce dev tutorials in your inbox

Comments

Loading comments...

Leave a Comment

Trending Now