Salesforce Development
Hands-onBeginner 40 min· Developer · 8 of 17

SOQL and SOSL

Learn to read Salesforce data with SOQL: filters, dates, relationship queries, aggregates and bind variables in Apex. Then use SOSL to search across objects.

SOQL (Salesforce Object Query Language) reads records from your org. It looks like SQL but is simpler: you query one object at a time, there are no joins (relationships do that job) and you can only read data, not change it. SOSL (Salesforce Object Search Language) searches text across many objects at once.

In this tutorial you will run queries in the Developer Console, filter and sort, follow relationships up and down, summarise data with GROUP BY, search with SOSL, and use SOQL inside Apex.

Prerequisites: a Developer Edition org with its sample data (Accounts, Contacts and Opportunities come pre-loaded) and a basic idea of objects and relationships from *Data model: objects, fields and relationships*.

Where to run queries

  1. 1Log in to your org, click the gear icon and choose Developer Console.
  2. 2In the Developer Console choose Query Editor at the bottom of the window.
  3. 3Type a query in the box and click Execute. Results appear in a grid below.
  4. 4Tick Use Tooling API only for Setup metadata; leave it off for normal data.

In VS Code with the Salesforce Extension Pack, the command SFDX: Execute SOQL Query and the SOQL Builder do the same job. Workbench also works. All of them run the same SOQL.

Your first queries

Basic query
SELECT Id, Name, Industry, AnnualRevenue
FROM Account

Returns every Account with four fields. Id is always available; other fields must be named explicitly. There is no SELECT *.

A query has a few clauses, always in this order: SELECT (fields), FROM (one object), WHERE (filter), GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET. You use only the ones you need. Keywords and field names are not case sensitive. Text comparisons with = are also case-insensitive, so 'acme' matches 'Acme'.

Filter, sort and limit
SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Industry = 'Technology'
  AND AnnualRevenue > 1000000
ORDER BY AnnualRevenue DESC
LIMIT 10

The ten largest technology accounts by revenue. Text values use single quotes. Numbers do not.

WHERE operators

OperatorMeaningExample
= and !=Equals, not equalsStageName = 'Closed Won'
<, >, <=, >=ComparisonAmount >= 50000
LIKEPattern match with % (any characters)Name LIKE 'Bean%'
IN and NOT INMatch any value in a listIndustry IN ('Technology', 'Banking')
AND, OR, NOTCombine conditions (use brackets)(Industry = 'Banking' OR Industry = 'Energy') AND AnnualRevenue > 0
INCLUDESMulti-select picklists onlyInterests__c INCLUDES ('Chai;Coffee')

Null checks use = null, not IS NULL: WHERE Phone = null finds accounts with no phone number.

Dates and date literals

Dates are written without quotes in the form 2026-10-31. Date-time fields use 2026-10-31T10:00:00Z. Salesforce also gives you date literals that move with the calendar, so a saved query never goes stale.

Date literals
SELECT Id, Name, Amount, CloseDate
FROM Opportunity
WHERE StageName = 'Closed Won'
  AND CloseDate = THIS_YEAR

SELECT Id, Name, CreatedDate
FROM Lead
WHERE CreatedDate = LAST_N_DAYS:30

THIS_YEAR, TODAY, YESTERDAY, THIS_MONTH, LAST_N_DAYS:n and NEXT_N_DAYS:n are common. Run the two queries one at a time in the Query Editor.

Relationship queries

You do not write joins. You walk along relationships instead, in two directions.

Child to parent (like a lookup): use dot notation on the relationship name. For a standard lookup such as Contact to Account, the name is Account. For a custom lookup field Venue__c, the relationship name is Venue__r. You can go up to five levels.

Parent to child: write a subquery in the SELECT list using the plural child relationship name, such as Contacts or Opportunities. For custom objects the child relationship name is set when you create the relationship, and usually ends in __r.

Both directions
-- child to parent: each contact with its account's name and industry
SELECT FirstName, LastName, Email, Account.Name, Account.Industry
FROM Contact
WHERE Account.Industry = 'Technology'

-- parent to child: each account with its contacts
SELECT Name, (SELECT FirstName, LastName FROM Contacts)
FROM Account
WHERE Industry = 'Technology'

The first returns a flat list of contacts. The second returns accounts, each holding a nested list of contacts.

Not sure of the relationship name? In Setup, open Object Manager > (object) > Fields & Relationships, click the lookup field and read the Child Relationship Name shown for the parent side. For custom lookups, the API name of the field ends in __c and the query name ends in __r.

Summarising with aggregate functions

COUNT(), SUM(), AVG(), MIN() and MAX() summarise many rows into one, and GROUP BY gives one row per group. HAVING filters groups after they are built (WHERE filters rows before).

Pipeline by stage
SELECT StageName, COUNT(Id) deals, SUM(Amount) total
FROM Opportunity
GROUP BY StageName
HAVING SUM(Amount) > 100000
ORDER BY SUM(Amount) DESC

One row per stage showing the number of opportunities and the total amount. Only stages totalling above 100,000 are kept. deals and total are aliases.

SELECT COUNT() FROM Contact (with empty brackets) just returns the number of records. In Apex that form is read as an Integer.

SOSL: searching across objects

Use SOQL when you know the object and the field. Use SOSL when you have a piece of text and want to find it anywhere, for example a customer types acme in a search box and you want matching Accounts, Contacts and Leads together. SOSL uses the search index, so it can be faster for text and it matches whole words and word stems.

SOSL search
FIND {acme} IN ALL FIELDS
RETURNING Account(Id, Name), Contact(Id, Name, Email), Lead(Id, Name, Company)

Searches every text field of the three objects. Wrap the search term in curly braces. The Developer Console Query Editor runs SOQL only, so run SOSL from Execute Anonymous (see the Apex example below) or from Workbench.

SOQL or SOSL

QuestionSOQLSOSL
SearchesOne object (plus related ones)Many objects at once
Best whenYou know the field and filterYou have a text term to find
ReturnsA list of recordsA list of lists, one list per object
FiltersAny field, any operatorMostly text; can add WHERE inside RETURNING
Text matchingLIKE with % wildcardsWord-based, from the search index

Using SOQL inside Apex

In Apex you put a query in square brackets. The result is a List of records (or a single record if you assign to one). Use a bind variable: a colon followed by an Apex variable. Bind variables are safe against injection and need no quotes. You can run this code in the Developer Console under Debug > Open Execute Anonymous Window; tick Open Log, run it, and read the lines with USER_DEBUG in the log.

Execute Anonymous
String industry = 'Technology';
Decimal minRevenue = 1000000;

List<Account> accts = [
    SELECT Id, Name, AnnualRevenue,
           (SELECT LastName FROM Contacts)
    FROM Account
    WHERE Industry = :industry AND AnnualRevenue > :minRevenue
    ORDER BY Name
    LIMIT 20
];

for (Account a : accts) {
    System.debug(a.Name + ' has ' + a.Contacts.size() + ' contacts');
}

// aggregate result
List<AggregateResult> rows = [
    SELECT StageName, COUNT(Id) deals, SUM(Amount) total
    FROM Opportunity
    GROUP BY StageName
];
for (AggregateResult ar : rows) {
    System.debug(ar.get('StageName') + ': ' + ar.get('deals') + ' deals, total ' + ar.get('total'));
}

// SOSL in Apex returns a list of lists
List<List<SObject>> found = [FIND 'acme' IN ALL FIELDS RETURNING Account(Name), Contact(Name)];
List<Account> foundAccounts = (List<Account>) found[0];
System.debug(foundAccounts.size() + ' accounts matched');

Prints one line per technology account, one line per opportunity stage, and the count of SOSL matches. Aggregate values come back as generic objects read with ar.get('alias').

SOQL injection

Never build a query by joining user text into a string, such as Database.query('SELECT Id FROM Account WHERE Name = \'' + userInput + '\''). A user can type text that changes the query (SOQL injection). Use bind variables. If you must build dynamic SOQL, bind variables still work inside Database.queryWithBinds, or wrap values in String.escapeSingleQuotes().

Security and limits to remember

In Apex, a plain query runs in system mode by default: it can read fields the user cannot see unless your class or query says otherwise. Add WITH USER_MODE to the query (for example ... FROM Account WITH USER_MODE) to enforce the user's field-level security and sharing. Learn more in *Security model: profiles, permission sets and sharing*.

Governor limits cap how much you can query in one transaction. In synchronous Apex you get 100 SOQL queries and 50,000 returned rows. You will meet them properly in *Apex triggers and governor limits*; the habit to start now is: never put a query inside a loop, and always filter.

How this comes up in interviews. Common questions: the difference between SOQL and SOSL; how to query parent from child and child from parent; what WHERE versus HAVING does; how to count records by group; what a bind variable is and why it matters; and how many SOQL queries Apex allows (100 synchronous). Be ready to write a subquery on a whiteboard.

Prompt · Explain or fix a query
You are a Salesforce developer who explains things simply. Here is my SOQL query:

[paste query]

It gives this error or wrong result: [paste error or describe it].

Explain what each clause does, find the problem, and give a corrected query. Mention any relationship names I should check in Object Manager.

What to expect: A good answer points to the exact clause at fault (for example a wrong relationship name or quotes around a number) and explains it. Test the corrected query in the Query Editor before trusting it.

Try it: Answer five questions with queries

Write and run a SOQL query for each:

1. Names of Contacts whose email is not empty, sorted by LastName. 2. Opportunities closing this month that are not Closed Won or Closed Lost. 3. Accounts with the names of their open Opportunities (subquery). 4. The number of Accounts per Industry, largest first. 5. A SOSL search for the word edge in Account and Contact names.

Key takeaways

  1. SOQL reads from one object with SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY and LIMIT. There is no SELECT * and no joins.
  2. Use dot notation to read parent fields and a subquery with the plural child name to read child records.
  3. WHERE filters rows before grouping; HAVING filters groups after.
  4. SOSL searches text across many objects and returns a list of lists.
  5. In Apex use bind variables with a colon, never string-joined user input, and keep queries out of loops.

Check yourself

Finishing the quiz marks the lesson as done.

1. You want each Contact together with its Account's Industry. Which query does that?

2. What is the difference between WHERE and HAVING?

3. A user types text in a search box and you need matching Accounts, Contacts and Leads in one call. What do you use?

4. Which is the safest way to use a variable in a query in Apex?

5. What does SELECT StageName, COUNT(Id) FROM Opportunity GROUP BY StageName return?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free