Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Apex

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

Every SOQL ORDER BY pattern in one place, from ASC/DESC and multi-column priority to NULLS FIRST vs NULLS LAST, sorting by formula and reference fields, and the limits the docs don't tell you.

The short answer

To sort by a custom field in ascending order with null records at the end, append ORDER BY CustomField__c ASC NULLS LAST (or Relationship__r.CustomField__c ASC NULLS LAST for parent lookups) to your SOQL query. Explicit NULLS LAST is required because ascending SOQL sorts place null values first by default.

Key takeaways 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. Override with NULLS FIRST or NULLS LAST when the placement carries meaning. Direction applies per column. ORDER BY A DESC, B sorts A descending and B ascending, because the DESC does not carry over. SOQL allows up to 32 sort keys, and sorting on a parent field uses the relationship name rather than the foreign key column.

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 ID or Unique
  • 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 DESC returns rows in some order when a batch of records shares the same timestamp. Add Id as 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__c forces 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), never AccountId.Name. If you are unsure, sf data query in 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.

Frequently asked questions

What is the SOQL ORDER BY syntax?

SOQL ORDER BY uses the same shape as SQL: SELECT fields FROM Object ORDER BY field [ASC|DESC] [NULLS FIRST|NULLS LAST]. You can chain up to 32 sort columns: ORDER BY Account.Industry, Amount DESC, CloseDate. ASC is the default direction; NULLS FIRST is the default null placement for ASC, NULLS LAST for DESC.

How do I sort SOQL results in descending order?

Append DESC to the column name: SELECT Id, Name, Amount FROM Opportunity ORDER BY Amount DESC. To break ties with a secondary sort, list more fields: ORDER BY Amount DESC, CloseDate ASC. The DESC keyword applies only to the immediately preceding column.

What does NULLS FIRST and NULLS LAST mean in SOQL?

Controls where rows with a null value in the sort column appear: NULLS FIRST puts them at the top, NULLS LAST puts them at the bottom. Useful when sorting on optional fields like CloseDate or LastActivityDate where you want pending records (null) handled deliberately. Default: NULLS FIRST for ASC, NULLS LAST for DESC.

Can I ORDER BY multiple columns in SOQL?

Yes. Comma-separate them with optional ASC/DESC per column: ORDER BY Account.Name ASC, Amount DESC, Id. The first column is the primary sort key; subsequent columns break ties. Soql supports up to 32 sort columns per query, but practically you rarely need more than 3.

Can I sort SOQL results by a parent object's field?

Yes, using dot notation: SELECT Id, Account.Name FROM Contact ORDER BY Account.Industry, Account.Name. The relationship name (Account, OpportunityLineItems, etc.) is required; you can't reference the lookup ID directly for sorting.

Why is my SOQL ORDER BY query slow?

Three common reasons: (1) the sort column isn't indexed (Salesforce auto-indexes some fields like Id, Name, OwnerId, RecordType, foreign keys, custom fields with External Id or Unique = true), and a non-indexed sort forces a full scan; (2) you're sorting a large result without a selective WHERE clause; (3) ORDER BY a formula field that itself contains uncached calculations. Use indexed fields whenever possible.

Can I use ORDER BY with aggregate SOQL queries?

Yes, and you must use the alias or the aggregate function: SELECT Industry, COUNT(Id) cnt FROM Account GROUP BY Industry ORDER BY COUNT(Id) DESC. You cannot ORDER BY a non-aggregated field that isn't in the GROUP BY clause.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment