Skip to main content
SFDC Developers
Apex

SOQL: The Complete Guide to Salesforce Object Query Language

Vinay Vernekar · · 6 min read

SOQL is the read-side of every Salesforce app — the query language that powers list views, reports, Apex, and every external integration. This is the pillar that ties together every detail-level guide on this site, organized by the questions you'll actually have.

What is SOQL?

SOQL (Salesforce Object Query Language) is a SQL-flavored, read-only query language designed for the Salesforce data model. It runs in Apex (List<Account> accounts = [SELECT Id FROM Account]), the Salesforce CLI (sf data query -q "SELECT Id FROM Account"), Workbench, and the REST /query endpoint. Every record-reading operation in Salesforce ultimately decomposes into SOQL.

Three things SQL users must adjust to:

  1. No JOIN syntax. SOQL traverses Salesforce's pre-defined relationships via dot notation (parent) or inner SELECT (children). You cannot join arbitrary tables.
  2. Read-only. INSERT/UPDATE/DELETE are DML in Apex (insert acc;), not SOQL.
  3. Sharing-aware. Queries automatically respect each user's record visibility unless explicitly overridden with WITH SECURITY_ENFORCED or without sharing Apex contexts.

Anatomy of a SOQL query

SELECT field, field, ...   -- columns to return
FROM ObjectName            -- one object per query
WHERE conditions            -- optional filter
WITH FILTER                -- optional access filter (PERMISSION_REQUIRED, etc.)
GROUP BY field             -- optional aggregation
HAVING aggregate-condition -- optional filter on aggregates
ORDER BY field [ASC|DESC]  -- optional sort
LIMIT n                    -- optional row cap
OFFSET n                   -- optional skip

Three of those clauses — WHERE, ORDER BY, and the aggregates — have enough nuance to deserve their own pages.

Filter operators (WHERE clause)

Operator Behavior Deep-dive
=, !=, <> Equality / inequality Below: null gotcha
<, >, <=, >= Numeric / date comparison
IN, NOT IN Match against a list or subquery SOQL NOT IN, NOT EQUAL & NOT LIKE
LIKE, NOT LIKE Substring with % and _ wildcards SOQL LIKE & NOT LIKE
INCLUDES, EXCLUDES Multi-select picklist SOQL CONTAINS vs LIKE vs INCLUDES
IS NULL, IS NOT NULL Null handling

The single biggest gotcha in SOQL: != and NOT IN silently exclude rows where the field is null. If you want to include nulls, add OR field = NULL explicitly. The deep-dive linked above walks through exactly when this bites.

Sorting and pagination

ORDER BY supports multi-column sorts, NULLS FIRST/LAST placement, descending direction, and sorting by parent-record fields:

SELECT Id, Name, Account.Industry
FROM Contact
ORDER BY Account.Industry ASC, LastName ASC NULLS LAST
LIMIT 100 OFFSET 200

LIMIT and OFFSET together provide pagination, but OFFSET caps at 2,000 rows — for deeper paging, use cursor-style queries with Id > instead. Full guide: SOQL ORDER BY: Sort, NULLS FIRST/LAST & Multi-Column.

Parent-child relationships

SOQL traverses Salesforce relationships in two directions:

Child to parent (one record up): use the relationship name in dot notation:

SELECT Id, Name, Account.Industry, Account.Owner.Email
FROM Contact

You can chain up to 5 levels: Account.Owner.Profile.Name.

Parent to child (many records down): use an inner SELECT with the child relationship name (plural, with s):

SELECT Id, Name, (SELECT Id, FirstName FROM Contacts)
FROM Account
WHERE Industry = 'Technology'

For custom relationships, swap suffix: Project__r for the parent reference, Projects__r for the child collection.

For cross-object full-text search across many objects at once, see SOSL — covered in SOQL CONTAINS vs LIKE vs INCLUDES.

Aggregate functions

Salesforce supports COUNT, COUNT_DISTINCT, SUM, AVG, MIN, and MAX. Aggregate queries return AggregateResult objects, not SObjects:

List<AggregateResult> revenue = [
  SELECT Industry, SUM(AnnualRevenue) totalRevenue, COUNT(Id) cnt
  FROM Account
  GROUP BY Industry
  HAVING COUNT(Id) > 5
  ORDER BY SUM(AnnualRevenue) DESC
];

for (AggregateResult ar : revenue) {
  String industry = (String) ar.get('Industry');
  Decimal total = (Decimal) ar.get('totalRevenue');
}

Three rules:

  • Every column not aggregated must appear in GROUP BY.
  • Use HAVING (not WHERE) to filter on aggregate results.
  • Aliases (totalRevenue, cnt) make extraction in Apex readable.

Governor limits

Limit Sync transaction Async (Batch / Future / Queueable)
Total SOQL queries 100 200
Total rows returned 50,000 50,000
Records in single query 50,000 unbounded with QueryLocator
OFFSET maximum 2,000 2,000
ORDER BY rows 32,000 32,000

Hit the row limit and Salesforce throws LIMIT_EXCEEDED. For larger result sets, switch to:

  • Batch Apex with Database.QueryLocator — handles unbounded rows in chunks of 200.
  • Apex cursors — Spring '24+, streams results without loading everything in memory.

Bind variables vs dynamic SOQL

Two ways to inject Apex variables into a query:

Bind variables (preferred): prefix with colon. Salesforce auto-escapes:

String myIndustry = 'Technology';
List<Account> accs = [SELECT Id FROM Account WHERE Industry = :myIndustry];

Dynamic SOQL (only when the query shape itself depends on input): use Database.query() with String.escapeSingleQuotes:

String safeName = String.escapeSingleQuotes(userInput);
String query = 'SELECT Id FROM Account WHERE Name LIKE \'%' + safeName + '%\'';
List<Account> accs = Database.query(query);

If you forget escapeSingleQuotes, SOQL injection becomes possible. See the Apex String class reference for the full pattern.

Deep-dive guides linked from this pillar

Common SOQL mistakes

  • Forgetting null handling on != / NOT IN. Add OR field = NULL if you want null rows.
  • Selecting too many fields. Each field is bytes returned to your transaction. Bring back only what you'll use.
  • Querying inside loops. Hits the 100-query limit instantly. Build collections, query once.
  • Leading % on LIKE for large objects. Defeats indexes; consider SOSL.
  • OFFSET past 2,000. Use WHERE Id > :lastId ORDER BY Id for cursor-style pagination instead.
  • Not using bind variables. Hardcoding values means you re-query on every change; binding makes the query reusable.

SOQL is one of those skills where the basics get you 80% of the way and the last 20% (null handling, governor limits, indexed sorts, parent-child traversal) decides whether your code survives at scale. Bookmark this pillar and the deep-dive links — between them, every SOQL question you'll have is a click away.

Share this article

Get weekly Salesforce dev tutorials in your inbox

Comments

Loading comments...

Leave a Comment

Trending Now