Database.query() with native bind variables or explicit bind maps, keeping your queries flexible, secure, and immune to SOQL injection.
Static SOQL queries inside square brackets (such as [SELECT Id FROM Account]) work well when object names, select fields, and filter clauses are fixed at compile time. However, building custom search screens, configurable export utilities, or polymorphic reporting tools requires dynamic query construction based on user input. Dynamic SOQL gives Apex developers complete runtime flexibility while maintaining strict security controls.
1. Static SOQL vs. Dynamic SOQL: Architectural Breakdown
Understanding when to use static vs. dynamic queries ensures optimal performance and code safety:
- Static SOQL: Verified at compile time. Syntax errors and invalid field references are flagged immediately during code compilation, and field references prevent custom fields from being deleted accidentally in Setup.
- Dynamic SOQL: Evaluated at runtime using
Database.query()orDatabase.getQueryLocator(). Useful when selectable fields, objects, or WHERE clauses depend on runtime configuration or custom metadata.
- Execution Methods:
Database.query(queryString, accessLevel)andDatabase.queryWithBinds(). - Bind Support: Simple in-scope variable resolution (
:myVar) and explicit Map-based binds (Map<String, Object>). - Security Standard: Built-in input sanitization using
String.escapeSingleQuotes()andAccessLevel.USER_MODE. - Governor Limits: Counts against the standard 100 SOQL query and 50,000 total retrieved rows limits.
2. Implementing Dynamic SOQL with Bind Variables
Salesforce natively resolves local Apex variables referenced with a colon (:variableName) directly inside dynamic query strings.
public with sharing class DynamicAccountSearchService {
public static List<Account> searchAccounts(String searchName, String industryFilter) {
// Prepare bind variables
String namePattern = '%' + String.escapeSingleQuotes(searchName.trim()) + '%';
// Construct query string with bind expressions
String dynamicQuery = 'SELECT Id, Name, Industry, AnnualRevenue ' +
'FROM Account ' +
'WHERE Name LIKE :namePattern ';
if (String.isNotBlank(industryFilter)) {
dynamicQuery += 'AND Industry = :industryFilter ';
}
dynamicQuery += 'ORDER BY Name ASC LIMIT 50';
// Execute query in User Mode to enforce FLS and CRUD rules
return Database.query(dynamicQuery, AccessLevel.USER_MODE);
}
}
3. Modern Enterprise Pattern: Database.queryWithBinds
In modern Apex architectures, building queries across loosely coupled layers or passing dynamic parameters without polluting local scope is handled using Database.queryWithBinds(). This method accepts an explicit Map<String, Object> of bind key-value pairs.
public with sharing class AdvancedDynamicFilterService {
public static List<Contact> filterContacts(Map<String, Object> filterCriteria) {
String baseQuery = 'SELECT Id, FirstName, LastName, Email, Title FROM Contact WHERE Id != NULL ';
Map<String, Object> bindVariables = new Map<String, Object>();
if (filterCriteria.containsKey('emailDomain')) {
baseQuery += 'AND Email LIKE :emailPattern ';
bindVariables.put('emailPattern', '%' + filterCriteria.get('emailDomain'));
}
if (filterCriteria.containsKey('title')) {
baseQuery += 'AND Title = :targetTitle ';
bindVariables.put('targetTitle', filterCriteria.get('title'));
}
baseQuery += 'LIMIT 100';
// Execute using explicit bind map and user-mode enforcement
return Database.queryWithBinds(baseQuery, bindVariables, AccessLevel.USER_MODE);
}
}
4. Preventing SOQL Injection Attacks
SOQL injection occurs when untrusted user input containing malicious SQL/SOQL commands is concatenated directly into a query string, altering the query logic.
String query = 'SELECT Id FROM Contact WHERE LastName = \'' + userInput + '\'';If
userInput is set to test' OR IsDeleted = FALSE OR LastName LIKE '%, the attacker bypasses the filter and retrieves unauthorized records.
- Prefer Bind Variables: Bind variables treat all inputs strictly as literals, eliminating SOQL injection vulnerabilities by design.
- Escape Single Quotes: If you must concatenate dynamic string values, wrap the variable in
String.escapeSingleQuotes(userInput)to neutralize injected closing quotes. - Enforce User Mode: Pass
AccessLevel.USER_MODEtoDatabase.query()so unauthorized users cannot access restricted objects or fields even if a query is modified.
5. Dynamic SOQL with Batch Apex & Query Locators
When processing large datasets in asynchronous Batch Apex, pass dynamic query strings directly into Database.getQueryLocator() to stream up to 50 million records without hitting heap size limits.
public with sharing class DynamicAccountBatchProcessor implements Database.Batchable<sObject> {
private String dynamicQueryString;
public DynamicAccountBatchProcessor(String customQuery) {
this.dynamicQueryString = customQuery;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(dynamicQueryString);
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
for (Account acc : scope) {
acc.Description = 'Processed via dynamic batch on ' + Date.today().format();
}
update as user scope;
}
public void finish(Database.BatchableContext bc) {
System.debug('Dynamic batch processing complete.');
}
}
Summary
Dynamic SOQL empowers Salesforce developers to build adaptable, runtime-configurable data queries. By adhering to modern security standards—including bind variables, Database.queryWithBinds(), strict input escaping, and AccessLevel.USER_MODE permissions—development teams can deliver flexible search and filtering capabilities while keeping enterprise applications secure and performant.