Skip to main content

Dynamic SOQL in Salesforce Apex: Bind Variables, String Queries & Security Best Practices

In plain words: Dynamic SOQL allows you to build database query strings on the fly in Apex code at runtime instead of hardcoding static queries. By evaluating user input, filter conditions, and runtime variables dynamically, you can pass a string query to 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() or Database.getQueryLocator(). Useful when selectable fields, objects, or WHERE clauses depend on runtime configuration or custom metadata.
360 Dynamic SOQL Architecture Card:
  • Execution Methods: Database.query(queryString, accessLevel) and Database.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() and AccessLevel.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.

Pattern A: Simple In-Scope Bind Variable Query
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.

Pattern B: Explicit Map-Based Binds with Database.queryWithBinds
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.

Vulnerable Code Trap (Raw String Concatenation):
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.
Core Rule: Never concatenate raw text inputs directly into SOQL strings. Always use colon bind variables (:variableName), Database.queryWithBinds, or sanitize strings using String.escapeSingleQuotes().
  • 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_MODE to Database.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.