SOQL Fundamentals

SOQL (Salesforce Object Query Language) reads records.

14.1 What is SOQL?

SOQL (Salesforce Object Query Language) reads records. It looks like SQL SELECT but has no SELECT *, no arbitrary joins (it uses relationships instead), and queries a single root object. In Apex, static SOQL goes inside square brackets and is checked at compile time.

To avoid an exception when a query may return no rows, assign to a list and check isEmpty().

List<Account> accounts = [
    SELECT Id, Name, Industry, AnnualRevenue
    FROM Account
    WHERE Industry = 'Technology' AND AnnualRevenue > 1000000
    ORDER BY AnnualRevenue DESC NULLS LAST
    LIMIT 50
];

Account single = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];   // QueryException if 0 rows
Integer total = [SELECT COUNT() FROM Contact WHERE Email != null];

14.2 Bind variables

Use :variable to inject Apex values safely (no SOQL injection, automatic escaping):

String industry = 'Banking';
Set<Id> ownerIds = new Set<Id>{ UserInfo.getUserId() };
Date since = Date.today().addDays(-30);
List<Account> recent = [
    SELECT Id, Name FROM Account
    WHERE Industry = :industry
      AND OwnerId IN :ownerIds
      AND CreatedDate >= :since
];
Integer lim = 5;
List<Contact> few = [SELECT Id FROM Contact LIMIT :lim];

14.3 WHERE clause operators

Amount >= 1000 LIKE (%, _ wildcards, case-insensitive) Name LIKE 'Acme%' IN / NOT IN

StageName IN ('Prospecting', 'Closed Won') INCLUDES / EXCLUDES

Multi-select picklists: Interests__c INCLUDES ('Golf;Tennis') NOT Name LIKE 'Test%' = null / != null Email != null

CreatedDate = LAST_N_DAYS:30, CloseDate = THIS_QUARTER, TODAY, YESTERDAY, NEXT_MONTH Semi-join / anti-join

Id IN (SELECT AccountId FROM Contact), Id NOT IN (SELECT AccountId FROM Opportunity)

14.4 Relationship queries

Child-to-parent (dot notation, up to 5 levels):

Parent-to-child (subquery with the child relationship name, usually plural):

List<Contact> contacts = [
    SELECT Id, LastName, Account.Name, Account.Owner.Name, Account.Parent.Name
    FROM Contact WHERE Account.Industry = 'Energy'
];
// custom lookup: Invoice__c.Customer__c → traverse with __r
List<Invoice__c> invoices = [SELECT Id, Customer__r.Name FROM Invoice__c];
List<Account> withKids = [
    SELECT Id, Name,
        (SELECT Id, LastName FROM Contacts ORDER BY LastName),
        (SELECT Id, Amount FROM Opportunities WHERE IsClosed = false),
        (SELECT Id FROM Invoices__r)                 // custom child relationship
    FROM Account
    WHERE Id IN (SELECT AccountId FROM Contact)
    LIMIT 100
];
for (Account a : withKids) {
    for (Contact c : a.Contacts) {
        System.debug(a.Name + ' → ' + c.LastName);
    }
}

14.5 Aggregate queries

Aggregate functions: COUNT(), COUNT(field), COUNT_DISTINCT(), SUM(), AVG(), MIN(), MAX(). Grouping extensions: GROUP BY ROLLUP(...), GROUP BY CUBE(...), GROUPING(). Date functions: CALENDAR_YEAR(CreatedDate), CALENDAR_MONTH(), DAY_ONLY(), FISCAL_QUARTER(). Unaliased aggregates are named expr0, expr1, ….

List<AggregateResult> results = [
    SELECT StageName, COUNT(Id) dealCount, SUM(Amount) total, AVG(Amount) average, MAX(CloseDate) latest
    FROM Opportunity
    WHERE CloseDate = THIS_YEAR
    GROUP BY StageName
    HAVING SUM(Amount) > 100000
    ORDER BY SUM(Amount) DESC
];
for (AggregateResult ar : results) {
    String stage = (String) ar.get('StageName');
    Integer cnt  = (Integer) ar.get('dealCount');
    Decimal sum  = (Decimal) ar.get('total');
    System.debug(stage + ': ' + cnt + ' deals, ' + sum);
}

14.6 Other clauses

ORDER BY field ASC|DESC NULLS FIRST|LAST Skip rows (max 2,000) — prefer keyset pagination for large sets

Lock rows for the transaction (avoid race conditions) Include deleted and archived records (Apex only) FOR VIEW / FOR REFERENCE

Update recently viewed / referenced data Polymorphic fields: TYPEOF What WHEN Account THEN Name, Phone WHEN Opportunity THEN Amount END WITH SECURITY_ENFORCED

Throw if the user lacks FLS/CRUD for referenced fields (older approach) WITH USER_MODE / WITH SYSTEM_MODE

Run the query in user mode (sharing + CRUD + FLS) or system mode (§15, §29)

FORMAT(), toLabel(), convertCurrency() Localized formatting, picklist labels, multi-currency conversion

Select all standard fields (FIELDS(ALL)/FIELDS(CUSTOM) require LIMIT 200 and are mainly for APIs)