Skip to content

Integrating SOQL with Apex: A Quick Guide

A query running inside an Apex transaction capsule with bound inputs, typed results, and governor-limit rings

Salesforce’s Apex programming language and SOQL (Salesforce Object Query Language) are core tools for building robust, data-driven applications on the platform. Understanding how to integrate SOQL effectively inside Apex is crucial for efficient retrieval, maintainable business logic, and predictable behaviour under governor limits. If you want a quick refresher before diving in, review SOQL fundamentals and filtering with the WHERE clause. If Apex itself is newer to you, Apex Fundamentals covers the language from the ground up. In this guide, we’ll cover practical SOQL-in-Apex patterns, common pitfalls, and failure handling without pretending that every platform limit can be caught.

SOQL queries Salesforce data the way SQL queries a relational database, so if you have written SQL the shape will look familiar. What matters in Apex is where it differs, because those differences decide how you write a query and how you stay out of trouble:

  • It runs against your org, under governor limits. Each transaction can issue only so many queries (100 in synchronous Apex) and return only so many rows (50,000). A query that is fine on ten records can fail on ten thousand, so most of the patterns below exist to get the data you need in as few queries as possible.
  • It returns sObjects, not rows. You get back strongly typed Account or Contact records you can read and update directly, not a generic result set you have to map.
  • It runs in a security context. Declare the class sharing model and each database operation’s access mode instead of relying on an API-version default. The SOQL security guide owns the complete API 67.0 model and migration detail.

Hold those three in mind and most of what follows is just applying them.

The sharpest example of this I have run into was not even my own code. I was pulled in to diagnose an Aura component that had run without complaint for over two years, then started erroring for users with nothing changed in it. The backend query had never carried a LIMIT, and the data behind it had finally grown past 50,000 rows, which is the most a single transaction can return. Helping fix it came down to two fronts: capping the query, and, the part that actually mattered, narrowing its WHERE clause so it only fetched the records that were genuinely needed rather than the whole set. A query can run fine for years and still be one busy quarter away from its limit.

Diagram titled One Transaction, Hard Limits: one Apex transaction running a SOQL query against Salesforce data and returning records, with gauges showing per-transaction limits of 1 of 100 SOQL queries and 200 of 50,000 rows returned, the row gauge marked with a 50,000 ceiling.

A simple SOQL query in Apex might look like this:

List<Account> accounts = [SELECT Id, Name FROM Account WHERE Industry = 'Technology'];

This query pulls the Id and Name of every Account in the ‘Technology’ industry. The square brackets are Apex’s inline SOQL syntax, and the result lands straight in a List<Account> you can work with in code, with no extra steps to open a connection or loop through rows yourself. That tight coupling between query and language is what makes data retrieval in Apex feel so direct.

Real queries rarely know their values when you write them. The industry comes from a user’s selection, the record Id from the page they are on, the set of Ids from an earlier query. A bind variable lets a query take those values at runtime: prefix an in-scope Apex variable with a colon inside the query, and the platform substitutes its value when the query runs.

String industry = 'Technology';
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Industry = :industry];

There are a few details worth pulling out:

  • The colon is the bind. :industry tells SOQL to read the Apex variable industry, not to match a field or a literal of that name. What SOQL binds is the value that the expression resolves to, so a simple variable (:industry), a field reference (:account.Industry), or a method call (:ids.size()) all work, but the colon binds a value, not a fragment of SOQL, so arithmetic or operators inside the query (for example :amount + 100) won’t work. Compute the value in Apex first, then bind it.
  • It is the safe way to use input. Because the platform binds the value instead of splicing text into the query, inline SOQL with bind variables cannot be hit by SOQL injection. That safety is the main reason to reach for inline SOQL before query strings.
  • You can bind collections. WHERE Id IN :accountIds binds a whole Set or List at once, which is how you filter a query by the results of a previous one instead of querying inside a loop:
Set<Id> accountIds = getTargetAccountIds();
List<Contact> contacts = [
SELECT Id, Name, AccountId
FROM Contact
WHERE AccountId IN :accountIds
];
Two-panel diagram titled Two Ways to Put a Value in a Query. The left panel, Bind variable (safe), shows an Apex variable flowing into the query WHERE Name = :searchName with a green shield and check, captioned that Salesforce treats the value as data, never as query text. The right panel, String concatenation (risky), shows user input glued into WHERE Name = ' + userInput with a broken padlock and a red SOQL injection tag, captioned that untrusted text becomes part of the query.

One query can reach across related objects, and that matters for the same reason everything else here does: the alternative, looking up each related record separately in a loop, is exactly what exhausts governor limits. Relationships run in two directions.

Child to parent uses dot notation. Starting from a Contact, Account.Name walks up the lookup to its parent account in the same query, and you can keep going, for example Account.Owner.Name:

List<Contact> contacts = [SELECT Id, Name, Account.Name FROM Contact WHERE Account.Industry = 'Technology'];

This query returns each contact with its parent account’s name attached, filtered by a field on the parent, all in a single query. Two things to keep straight:

  • The relationship name is not always the object name. Standard lookups usually use the object name (Account), while custom relationships end in __r, such as Order__r.Name.
  • You can traverse up to five levels from the starting object, so Account.Owner.Profile.Name is fine, but there is a ceiling.

Parent to child runs the other way, using a subquery, which the next section covers.

If you want to go deeper on traversal patterns and relationship names, see SOQL relationship query examples.

When you need to filter records based on multiple criteria, you can use logical operators like AND and OR in your SOQL queries. Here’s an example:

// Query accounts in the 'Technology' industry with annual revenue greater than $1,000,000
List<Account> techAccounts = [SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Industry = 'Technology'
AND AnnualRevenue > 1000000];
// Iterate through the results and process each account
for (Account acc : techAccounts) {
System.debug('Account Name: ' + acc.Name + ', Revenue: ' + acc.AnnualRevenue);
}

This query filters accounts by Industry and AnnualRevenue. The logical operator AND is used to combine conditions, ensuring both must be true for a record to be included.

For a focused filtering refresher, review SOQL WHERE clause patterns and advanced SOQL filtering techniques.

Subqueries allow you to retrieve related records in a single query. This is particularly useful for parent-child relationships. Here’s an example:

// Query accounts and their related contacts
List<Account> accountsWithContacts = [SELECT Id, Name, (SELECT Id, FirstName, LastName FROM Contacts)
FROM Account
WHERE Industry = 'Technology'];
// Iterate through accounts and their related contacts
for (Account acc : accountsWithContacts) {
System.debug('Account Name: ' + acc.Name);
for (Contact con : acc.Contacts) {
System.debug('Contact Name: ' + con.FirstName + ' ' + con.LastName);
}
}

The subquery (SELECT Id, FirstName, LastName FROM Contacts) retrieves related Contact records for each Account. This approach minimises the number of queries needed and can improve efficiency.

Diagram titled One Query, Parents and Children Together. A single SOQL query boundary holds one parent Account record connected by short lines to three child Contact records, labelled with the subquery SELECT Name, (SELECT LastName FROM Contacts) FROM Account, captioned that a subquery fetches the parent and its children in one trip. A faded alternative behind it shows the same parent with many separate query arrows, labelled Many separate queries.

By using these techniques, you can construct complex queries that efficiently retrieve the data needed for your Salesforce applications. These examples illustrate the flexibility and power of SOQL when integrated with Apex, enabling developers to build sophisticated data-driven solutions.

  1. Governor Limits Awareness: Salesforce imposes limits on the number of queries and records processed. Always be mindful of these limits to avoid runtime exceptions.
  2. Efficient Querying: Retrieve only the necessary fields and records to optimise performance.
  3. Bulk Processing: Use collections and bulk processing techniques to handle large data volumes efficiently.
  4. Use of Indexes: Use selective filters and inspect the Query Plan where volume matters; an indexed field is not automatically selective.
  5. Failure Handling: Prevent predictable query failures first, then catch only the exceptions the caller can recover from.
  6. Security Context Awareness: Declare both the class sharing model and the database operation’s access mode; prefer WITH USER_MODE for user-facing queries.

For the version history, trigger exception, class/query interaction matrix, and choice between query-time rejection and a partial response, use the SOQL security guide as the canonical reference. This page keeps the focus on writing and handling queries inside Apex.

As your queries grow, pair these practices with SOQL performance optimisation strategies and SOQL security controls to avoid regressions in scale and access behaviour.

  • Data Retrieval: Fetching records for display in Visualforce pages or Lightning components.
  • Data Manipulation: Using queried data to perform business logic or calculations.
  • Integration: Retrieving data for integration with external systems or processes. For API-oriented patterns, see SOQL API integration guide.

Reliable Apex separates failures you can prevent from failures a caller can genuinely recover from. QueryException covers runtime query problems, but it is not a catch-all for governor limits.

QueryException is an exception that occurs when there is an issue with a SOQL query in Apex. Common causes include:

  • Assigning a query with zero or multiple rows directly to one sObject variable.
  • Malformed dynamic SOQL, because its structure is checked at runtime rather than compile time.
  • Object- or field-access failures from a query running in user mode.

Static SOQL with a misspelled field or invalid syntax normally fails when the class compiles. Exceeding a governor limit raises System.LimitException, which Apex cannot catch. The remedy for limits is bulk-safe design, not a surrounding try block.

The first one of these to catch me read List has no rows for assignment to SObject. I was working in an org whose older functionality looked up reference data from custom settings. The code had run cleanly for a long time, and none of us had clocked that it was assigning a query straight into a single record:

// This will throw "List has no rows for assignment to SObject"
// if no matching configuration record exists.
Custom_Setting__c config = [
SELECT Id, Name
FROM Custom_Setting__c
WHERE Name = 'Reference_1'
LIMIT 1
];

Assigning a query into a single Custom_Setting__c variable only works when the query returns exactly one row. The day someone deleted that custom setting, the query returned nothing, the assignment had no row to bind to, and users started seeing the exception. The fix was small: query into a list, check whether anything came back, and handle the missing record on purpose instead of assuming it would always be there.

List<Custom_Setting__c> configs = [
SELECT Id, Name
FROM Custom_Setting__c
WHERE Name = 'Reference_1'
LIMIT 1
];
if (configs.isEmpty()) {
// Handle the missing configuration deliberately, do not assume a row.
return;
}
Custom_Setting__c config = configs[0];

Catch QueryException only where you can handle it intentionally, such as adding safe context, returning a user-friendly error, or applying a documented fallback. Do not wrap every query automatically; let unexpected failures surface so defects are not hidden.

public with sharing class AccountService {
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts(List<Id> accountIds) {
try {
return [
SELECT Id, Name, AnnualRevenue
FROM Account
WHERE Id IN :accountIds
WITH USER_MODE
];
} catch (QueryException e) {
// Log e using your server-side logging standard.
throw new AuraHandledException(
'Accounts could not be loaded with your current access.'
);
}
}
}

🛟 Best Practices for Exception Handling

Section titled “🛟 Best Practices for Exception Handling”
  • Prevent known shapes: Query into a list when zero rows are legitimate, validate dynamic identifiers against an allowlist, and use binds for values.
  • Catch only recoverable failures: A catch block should have a defined outcome, not just print a debug line and continue with incomplete data.
  • Log safely: Record enough server-side context to investigate without exposing raw queries, record data, or permission details to the user.
  • Design around limits: LimitException cannot be caught, so bulkify queries, keep them outside loops, and monitor row counts before the transaction reaches the ceiling.

The list-assignment example above is the important pattern: remove avoidable exceptions from normal control flow, and reserve try/catch for boundaries where recovery is deliberate.

🕒 When should I use inline SOQL versus Database.query() in Apex?

Section titled “🕒 When should I use inline SOQL versus Database.query() in Apex?”

Use inline SOQL for fixed, compile-time queries because it is easier to read and checked at compile time. Use Database.query() for dynamic SOQL when fields, objects, or filters must be assembled at runtime.

📉 How do I avoid hitting SOQL governor limits in triggers?

Section titled “📉 How do I avoid hitting SOQL governor limits in triggers?”

Bulkify your logic by querying once per object type, moving SOQL outside loops, and working with collections. Filter selectively and retrieve only fields you need. Triggers, Limits & Bulk Patterns walks through the full bulkification pattern.

🔐 Should I prefer WITH USER_MODE or WITH SECURITY_ENFORCED?

Section titled “🔐 Should I prefer WITH USER_MODE or WITH SECURITY_ENFORCED?”

Prefer WITH USER_MODE for new user-facing queries and treat WITH SECURITY_ENFORCED as legacy. The SOQL security guide explains why this is an upgrade rather than a direct rename, how API 67.0 changed the defaults, and what to test when migrating older classes.

🚀 What is the fastest way to troubleshoot a slow SOQL query in Apex?

Section titled “🚀 What is the fastest way to troubleshoot a slow SOQL query in Apex?”

Use the Query Plan tool to inspect selectivity, cardinality, and cost, then improve filters, reduce returned fields, and verify index usage.


Reliable SOQL in Apex comes down to a few reviewable decisions: bind values, query once for a collection, select only what the use case needs, declare the access mode, and treat governor limits as design constraints rather than catchable errors. The strongest code also defines what zero rows, missing permissions, and growing data volume should do before those conditions reach production.

Next on the advanced learning path is FOR VIEW, FOR REFERENCE, and FOR UPDATE, which extends inline SOQL with usage tracking and row locking.