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:
- No JOIN syntax. SOQL traverses Salesforce's pre-defined relationships via dot notation (parent) or inner SELECT (children). You cannot join arbitrary tables.
- Read-only. INSERT/UPDATE/DELETE are DML in Apex (
insert acc;), not SOQL. - Sharing-aware. Queries automatically respect each user's record visibility unless explicitly overridden with
WITH SECURITY_ENFORCEDorwithout sharingApex 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(notWHERE) 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
- SOQL LIKE & NOT LIKE: Wildcards, Examples & Best Practices
- SOQL ORDER BY: Sort, NULLS FIRST/LAST & Multi-Column Examples
- SOQL NOT IN, NOT EQUAL & NOT LIKE: Exclusion Patterns
- SOQL CONTAINS vs LIKE vs INCLUDES: Which Operator to Use
- Stream large datasets with Apex Cursors
- Apex String class reference (escape, format, split, join)
- Salesforce Order of Execution (where SOQL fits in the save lifecycle)
Common SOQL mistakes
- Forgetting null handling on
!=/NOT IN. AddOR field = NULLif 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 Idfor 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.
Leave a Comment