SOQL is the read side of every Salesforce app. It's the query language behind 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.
If you come to it from SQL, three things need unlearning:
- There is no JOIN syntax. SOQL traverses Salesforce's pre-defined relationships via dot notation (parent) or inner SELECT (children). You cannot join arbitrary tables.
- It is read-only. INSERT/UPDATE/DELETE are DML in Apex (
insert acc;), not SOQL. - It is 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 have enough nuance to deserve their own pages: WHERE, ORDER BY, and the aggregates.
Filter operators (WHERE clause)
| Operator | Behavior | Deep-dive |
|---|---|---|
=, !=, <> |
Equality / inequality | Below: null gotcha |
<, >, <=, >= |
Numeric / date comparison | n/a |
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 | n/a |
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 give you 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) uses 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) uses 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, use SOSL instead. It's 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, which handles unbounded rows in chunks of 200. - Apex cursors, available from Spring '24, which stream results without loading everything in memory.
Bind variables vs dynamic SOQL
Two ways to inject Apex variables into a query:
Bind variables are the option to reach for first. Prefix with a colon, and Salesforce auto-escapes:
String myIndustry = 'Technology';
List<Account> accs = [SELECT Id FROM Account WHERE Industry = :myIndustry];
Dynamic SOQL is for the case where 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
!=andNOT 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, so 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, and 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 above. Between them, every SOQL question you'll have is a click away.
Leave a Comment