String SOQL Queries in Salesforce Apex [Complete Guide with Examples]

This is one of the most important but confusing topics in Apex for many Salesforce developers.

Most beginners know SOQL. But very few understand how to build SOQL using Strings (Dynamic SOQL).

In real-time projects, you cannot always write fixed SOQL. You often need to build queries based on user input, conditions, filters, picklist values, or configuration.

This is where String Query in Apex (Dynamic SOQL) becomes very powerful. In this complete tutorial, you will learn String SOQL queries in Salesforce Apex.

  • What is a string query in Apex
  • Difference between static SOQL and Dynamic SOQL
  • How to create SOQL using a string
  • How to pass variables inside a string query
  • How to add WHERE conditions dynamically
  • How to add fields dynamically
  • How to prevent SOQL Injection
  • Real-time practical examples
  • Best practices used in real projects

Let’s start from the basics.

What is a String Query in Apex?

A string query in Apex is writing SOQL inside a string variable and then executing it with Database.query().

Instead of writing a fixed SOQL like this:

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

We write it like this:

String q = 'SELECT Id, Name FROM Account WHERE Industry = \'IT\'';
List<Account> accList = Database.query(q);

Here, the SOQL is stored inside a string and executed at runtime. This is called Dynamic SOQL.

Notice the difference? You’re constructing the query as text, then executing it with Database.query().

Why does this matter? Because now you can change that string before you run it. You can add fields, swap filters, or build entirely different queries based on logic in your code.

When Should You Use String Queries in Salesforce Apex?

Don’t use dynamic SOQL everywhere. Static queries are faster, safer, and easier to read. But there are times when string queries are the right tool:

  • User-driven filters: When users pick fields or criteria from a UI
  • Configurable reports: When admins configure what data to pull
  • Generic utility classes: When you’re writing reusable code that works across objects
  • Complex conditional logic: When your WHERE clause changes based on multiple factors
  • Unknown fields at compile time: When you don’t know which fields you’ll need until runtime

If your query is simple and never changes, stick with static SOQL. But if you need flexibility, string queries are your friend.

String SOQL Queries in Salesforce Apex

Let’s start simple. Here’s how to build and execute a basic string query:

String queryString = 'SELECT Id, Name FROM Account LIMIT 10';
List<Account> accounts = Database.query(queryString);

for(Account acc : accounts) {
    System.debug(acc.Name);
}

That’s it. You write the query as a string, pass it to Database.query(), and you get back a list of sObjects.

A few things to notice:

  • The query string is in single quotes
  • You use Database.query() to execute it
  • The return type is List<sObject>, which you can cast to List<Account> if needed
String SOQL Queries in Salesforce Apex

Build Dynamic Queries with Variables in Salesforce Apex

Now let’s make it useful. Say you want to filter by industry, but that industry comes from a variable:

String selectedIndustry = 'Healthcare';
String queryString = 'SELECT Id, Name FROM Account WHERE Industry = \'' + selectedIndustry + '\'';
List<Account> accounts = Database.query(queryString);
system.debug(queryString);

See those backslashes and quotes? \' is how you escape single quotes inside a string. Your final query string looks like:

SELECT Id, Name FROM Account WHERE Industry = 'Healthcare'

It’s a bit ugly, but it works. You’re inserting the variable value into the query string.

Build Dynamic Queries with Variables in Salesforce Apex

Add Multiple Conditions to String Queries in Salesforce Apex

Real-world queries are rarely simple. You’ll often need multiple filters. Here’s how to handle that:

String industry = 'Technology';
String state = 'California';

String queryString = 'SELECT Id, Name FROM Account';
queryString += ' WHERE Industry = \'' + industry + '\'';
queryString += ' AND BillingState = \'' + state + '\'';
queryString += ' ORDER BY Name';
queryString += ' LIMIT 50';

List<Account> accounts = Database.query(queryString);
system.debug(queryString);

This approach builds the query piece by piece. It’s cleaner and easier to debug than cramming everything into one line.

Add Multiple Conditions to String Queries in Salesforce Apex

Handle Optional Filters in String Query in Salesforce Apex

This is where dynamic queries really shine. What if some filters are optional?

String queryString = 'SELECT Id, Name, Industry FROM Account WHERE Id != null';

if(String.isNotBlank(industry)) {
    queryString += ' AND Industry = \'' + industry + '\'';
}

if(String.isNotBlank(state)) {
    queryString += ' AND BillingState = \'' + state + '\'';
}

List<Account> accounts = Database.query(queryString);

for(Account acc : accounts) {
    System.debug(acc.Name + ' ' + acc.industry + ' ' + acc.state);
}

The trick here is starting with a condition that’s always true (WHERE Id != null). Then you can safely add AND for each optional filter.

Handle Optional Filters in String Query in Salesforce Apex

Alternatively, you can track whether it’s the first condition:

public class StringQuery {
    
    Public static void runQuery(){
        String industry = 'Technology';
		String state = 'Texas';
        
        String queryString = 'SELECT Id, Name, Industry, BillingState FROM Account';
        Boolean hasWhere = false;
        
        if(String.isNotBlank(industry)) {
            queryString += ' WHERE Industry = \'' + industry + '\'';
            hasWhere = true;
        }
        
        if(String.isNotBlank(state)) {
            queryString += (hasWhere ? ' AND' : ' WHERE') + ' BillingState = \'' + state + '\'';
            hasWhere = true;
        }
        system.debug(queryString);
        List<Account> accounts = Database.query(queryString);
        
        for(Account acc : accounts) {
	    System.debug(acc.Name + ' ' + acc.industry + ' ' + acc.BillingState);
        }
    }
}

hasWhere concept in 5 simple points:

  • Result: No matter which condition runs first (Industry or State), the query always uses correct syntax (WHERE first, AND next).
  • Purpose: hasWhere remembers whether the query already contains a WHERE clause.
  • Initial State: It starts as false, indicating that no WHERE clause has been added yet.
  • When the first condition is added, the code adds WHERE … and sets hasWhere = true.
  • When the next condition is added, the code checks hasWhere:
    • If true → add AND
    • If false → add WHERE
hashere Condition in String Query in Salesforce Apex

Query Dynamic Field Selection in Salesforce Apex

You can also build the field list dynamically:

public class StringQuery {
    
    Public static void runQuery(){
        String industry = 'Technology';
		String state = 'Texas';
        
        List<String> fields = new List<String>{'Id', 'Name', 'Industry', 'BillingState'};
        String fieldString = String.join(fields, ', ');

        String queryString = 'SELECT ' + fieldString + ' FROM Account LIMIT 10';

        System.debug('Query: ' + queryString);

        List<Account> accounts = Database.query(queryString);

        for(Account acc : accounts) {
            System.debug(acc.Id + ' ' + acc.Name + ' ' + acc.Industry + ' ' + acc.BillingState);
        }
    }
}

This is useful when users select which fields they want to see, or when you’re building a generic query builder.

Query Dynamic Field Selection in Salesforce Apex

SOQL Injection in Salesforce Apex: The Big Security Risk

Here’s the scary part. If you’re not careful, string queries can create security holes.

Imagine this code:

public class StringQuery {

    public static void runQuery() {

        String userInput;

        if (ApexPages.currentPage() != null) {
            userInput = ApexPages.currentPage().getParameters().get('search');
        }

        String queryString = 'SELECT Id, Name FROM Account';

        if (String.isNotBlank(userInput)) {
            queryString += ' WHERE Name = :userInput';
        }

        List<Account> accounts = Database.query(queryString);

        for(Account acc : accounts) {
            System.debug(acc.Id + ' ' + acc.Name);
        }
    }
}
SOQL Injection in Salesforce Apex

Looks innocent, right? But what if a user types this:

test' OR '1'='1

Your query becomes:

SELECT Id, Name FROM Account WHERE Name = 'test' OR '1'='1'

That returns every single Account in your org. The user just bypassed your filter.

This is called SOQL injection, and it’s dangerous.

Prevent SOQL Injection in Salesforce Apex

Always clean user input before putting it in a query. Here’s how:

Use String.escapeSingleQuotes()

String userInput = ApexPages.currentPage().getParameters().get('search');
String safeInput = String.escapeSingleQuotes(userInput);
String queryString = 'SELECT Id, Name FROM Account WHERE Name = \'' + safeInput + '\'';

This method escapes any single quotes in the input, neutralizing injection attempts.

Use bind variables when possible

For some scenarios, you can still use bind variables with dynamic queries:

String userInput = 'Technology';
String queryString = 'SELECT Id, Name FROM Account WHERE Industry = :userInput';
List<Account> accounts = Database.query(queryString);

Bind variables (the:userInput part) are automatically safe from injection. But this only works when the variable is in scope.

Whitelist inputs

If users are selecting from predefined options, validate against a whitelist:

Set<String> allowedIndustries = new Set<String>{'Technology', 'Healthcare', 'Finance'};
String userInput = 'Technology';

if(!allowedIndustries.contains(userInput)) {
    throw new IllegalArgumentException('Invalid industry');
}

String queryString = 'SELECT Id, Name FROM Account WHERE Industry = \'' + userInput + '\'';

Working with Different sObject Types in String Queries in Apex

Sometimes you don’t know which object you’re querying until runtime:

public class StringQuery {

    public static void runQuery() {

        String objectName = 'Account';
        String queryString = 'SELECT Id, Name FROM ' + objectName + ' LIMIT 10';

        System.debug('Query: ' + queryString);

        List<sObject> records = Database.query(queryString);

        for(sObject rec : records) {
            System.debug(rec.get('Name'));
        }
    }
}

Notice the return type is List<sObject>, not List<Account>. Since the object is dynamic, you can’t use a specific type.

To access fields, use the get() method:

String name = (String)rec.get('Name');
Working with Different sObject Types in String Queries in Apex

Using Database.query() vs Database.queryWithBinds() in Salesforce Apex

Starting in API version 43.0, Salesforce introduced Database.queryWithBinds() for better security:

String industry = 'Technology';
String queryString = 'SELECT Id, Name FROM Account WHERE Industry = :industry';

Map<String, Object> bindVars = new Map<String, Object>{'industry' => industry};
List<Account> accounts = Database.queryWithBinds(queryString, bindVars, AccessLevel.USER_MODE);

This method:

  • Keeps bind variables separate from the query string
  • Supports user mode vs system mode
  • Makes your code more readable and secure

Debugging Your String Queries

When your query doesn’t work, the first step is seeing what you actually built:

String queryString = 'SELECT Id, Name FROM Account WHERE Industry = \'' + industry + '\'';
System.debug('Query: ' + queryString);
List<Account> accounts = Database.query(queryString);

Check the debug logs. You’ll often find:

  • Missing spaces between keywords
  • Unescaped quotes
  • Invalid field names
  • Typos in object names

Copy the logged query and test it in the Developer Console’s Query Editor to isolate issues.

Practical Example: Search with Multiple Filters in String Queries in Salesforce Apex

Let’s put it all together. Here’s a real-world search method:

public class StringQuery {

    public static void runQuery() {

        String name = 'Tech';
        String industry = 'Technology';
        String state = 'Texas';

        String queryString = 'SELECT Id, Name, Industry, BillingState FROM Account';
        List<String> conditions = new List<String>();

        if(String.isNotBlank(name)) {
            conditions.add('Name LIKE \'%' + String.escapeSingleQuotes(name) + '%\'');
        }

        if(String.isNotBlank(industry)) {
            conditions.add('Industry = \'' + String.escapeSingleQuotes(industry) + '\'');
        }

        if(String.isNotBlank(state)) {
            conditions.add('BillingState = \'' + String.escapeSingleQuotes(state) + '\'');
        }

        if(!conditions.isEmpty()) {
            queryString += ' WHERE ' + String.join(conditions, ' AND ');
        }

        queryString += ' ORDER BY Name LIMIT 100';

        System.debug('Query: ' + queryString);

        List<Account> accounts = Database.query(queryString);

        for(Account acc : accounts) {
            System.debug(acc.Name + ' ' + acc.Industry + ' ' + acc.BillingState);
        }
    }
}

This method:

  • Accepts three optional parameters
  • Builds conditions only for the provided values
  • Escapes all user input
  • Joins conditions cleanly
  • Returns up to 100 results

Clean, safe, and flexible.

Search with Multiple Filters in String Queries in Salesforce Apex

Governor Limits to Watch for SOQL Queries in Apex

Dynamic queries count against the same governor limits as static queries:

  • 100 SOQL queries per transaction (synchronous)
  • 50,000 records retrieved per transaction
  • Query timeout after a few seconds for complex queries

Building huge WHERE clauses or selecting unnecessary fields will hurt performance just like static queries.

Common Mistakes to Avoid in Dynamic Queries in Apex

Here are the traps I see developers fall into:

  • Forgetting to escape quotes: Always use \' or String.escapeSingleQuotes()
  • Missing spaces: ‘SELECT Id FROM Account’ + ‘WHERE Name = …’ creates Account WHERE without a space
  • Not validating user input: Never trust data coming from users—always sanitize it
  • Building overly complex queries: Just because you can doesn’t mean you should. Sometimes multiple simple queries are better
  • Forgetting LIMIT clauses: Without limits, you might hit governor limits fast
  • Using dynamic queries when static would work: Static queries are optimized better by Salesforce

Test String Queries in Salesforce Apex

Testing dynamic SOQL is straightforward, but you need to cover different scenarios:

@isTest
public class AccountSearchControllerTest {

    @testSetup
    static void setup() {
        List<Account> accounts = new List<Account>();
        accounts.add(new Account(Name = 'Tech Corp', Industry = 'Technology', BillingState = 'California'));
        accounts.add(new Account(Name = 'Health Inc', Industry = 'Healthcare', BillingState = 'Texas'));
        accounts.add(new Account(Name = 'Tech Solutions', Industry = 'Technology', BillingState = 'New York'));
        insert accounts;
    }

    @isTest
    static void testSearchByName() {
        Test.startTest();
        List<Account> results = AccountSearchController.searchAccounts('Tech', null, null);
        Test.stopTest();

        System.assertEquals(2, results.size(), 'Should find 2 accounts with Tech in name');
    }

    @isTest
    static void testSearchByIndustry() {
        Test.startTest();
        List<Account> results = AccountSearchController.searchAccounts(null, 'Technology', null);
        Test.stopTest();

        System.assertEquals(2, results.size(), 'Should find 2 Technology accounts');
    }

    @isTest
    static void testSearchMultipleFilters() {
        Test.startTest();
        List<Account> results = AccountSearchController.searchAccounts('Tech', 'Technology', 'California');
        Test.stopTest();

        System.assertEquals(1, results.size(), 'Should find 1 account matching all criteria');
        System.assertEquals('Tech Corp', results[0].Name);
    }

    @isTest
    static void testSearchNoFilters() {
        Test.startTest();
        List<Account> results = AccountSearchController.searchAccounts(null, null, null);
        Test.stopTest();

        System.assertEquals(3, results.size(), 'Should return all accounts');
    }

    @isTest
    static void testSQLInjectionPrevention() {
        Test.startTest();
        // Attempt SQL injection
        List<Account> results = AccountSearchController.searchAccounts('test\' OR \'1\'=\'1', null, null);
        Test.stopTest();

        // Should return 0 results because the injection was escaped
        System.assertEquals(0, results.size(), 'Injection attempt should be neutralized');
    }
}

Make sure you test:

  • Each filter individually
  • Multiple filters combined
  • No filters at all
  • Edge cases like empty strings
  • Security scenarios (injection attempts)

Dynamic SOQL with Related Objects in Salesforce Apex

You can build queries with relationship fields too:

public class StringQuery {

    public static void runQuery() {

        String industry = 'Technology';

        // Dynamic SOQL with Parent → Child subquery
        String queryString = 
            'SELECT Id, Name, (SELECT Id, FirstName, LastName FROM Contacts) FROM Account';

        queryString += ' WHERE Industry = \'' + String.escapeSingleQuotes(industry) + '\'';

        System.debug('Query: ' + queryString);

        List<Account> accounts = Database.query(queryString);

        for(Account acc : accounts) {
            System.debug('Account: ' + acc.Name);

            for(Contact con : acc.Contacts) {
                System.debug('Contact: ' + con.FirstName + ' ' + con.LastName);
            }
        }
    }
}
Dynamic SOQL with Related Objects in Salesforce Apex

Or query child-to-parent:

String queryString = 'SELECT Id, FirstName, LastName, Account.Name, Account.Industry FROM Contact';
queryString += ' WHERE Account.Industry = \'' + String.escapeSingleQuotes(industry) + '\'';

List<Contact> contacts = Database.query(queryString);

The same rules apply—escape your inputs and watch for injection risks.

Using Database.queryLocator for Batch Jobs in Apex

When you’re working with batch Apex, you often need dynamic queries in the start() method:

public class AccountBatchProcessor implements Database.Batchable<sObject> {

    private String industry;

    public AccountBatchProcessor(String industry) {
        this.industry = industry;
    }

    public Database.QueryLocator start(Database.BatchableContext bc) {
        String queryString = 'SELECT Id, Name, Industry FROM Account';

        if(String.isNotBlank(industry)) {
            queryString += ' WHERE Industry = \'' + String.escapeSingleQuotes(industry) + '\'';
        }

        return Database.getQueryLocator(queryString);
    }

    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // Process accounts
        for(Account acc : scope) {
            // Do something
        }
    }

    public void finish(Database.BatchableContext bc) {
        // Finish logic
    }
}

You can pass parameters to your batch class constructor and use them to build the query dynamically.

Dynamic SOQL with IN Clauses in Salesforce Apex

Working with lists in WHERE clauses needs special handling:

List<String> industries = new List<String>{'Technology', 'Healthcare', 'Finance'};

// Escape each value
List<String> escapedIndustries = new List<String>();
for(String ind : industries) {
    escapedIndustries.add('\'' + String.escapeSingleQuotes(ind) + '\'');
}

String queryString = 'SELECT Id, Name FROM Account';
queryString += ' WHERE Industry IN (' + String.join(escapedIndustries, ',') + ')';

List<Account> accounts = Database.query(queryString);

This builds a query like:

SELECT Id, Name FROM Account WHERE Industry IN ('Technology','Healthcare','Finance')

You can also use bind variables for this:

Set<String> industries = new Set<String>{'Technology', 'Healthcare'};
String queryString = 'SELECT Id, Name FROM Account WHERE Industry IN :industries';
List<Account> accounts = Database.query(queryString);

Bind variables work as long as the variable is in scope.

Build ORDER BY Dynamically in Salesforce Apex

Let users sort results their way:

public static List<Account> searchAccounts(String industry, String sortField, String sortOrder) {

    // Whitelist allowed sort fields
    Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'CreatedDate', 'AnnualRevenue'};
    Set<String> allowedOrders = new Set<String>{'ASC', 'DESC'};

    if(!allowedFields.contains(sortField)) {
        sortField = 'Name'; // Default
    }

    if(!allowedOrders.contains(sortOrder)) {
        sortOrder = 'ASC'; // Default
    }

    String queryString = 'SELECT Id, Name, Industry, CreatedDate, AnnualRevenue FROM Account';

    if(String.isNotBlank(industry)) {
        queryString += ' WHERE Industry = \'' + String.escapeSingleQuotes(industry) + '\'';
    }

    queryString += ' ORDER BY ' + sortField + ' ' + sortOrder;
    queryString += ' LIMIT 100';

    return Database.query(queryString);
}

Notice the whitelist validation. Never trust user input for field names or sort direction. A malicious user could inject invalid syntax.

Build ORDER BY Dynamically in Salesforce Apex

Performance Tips

Dynamic queries can be slower than static ones if you’re not careful:

Select only what you need

// Bad - selecting everything
String queryString = 'SELECT FIELDS(ALL) FROM Account LIMIT 100';

// Good - select specific fields
String queryString = 'SELECT Id, Name, Industry FROM Account LIMIT 100';

Use indexes when possible

Filter on indexed fields like Id, Name, CreatedDate, or custom fields marked as External ID:

// This will perform better
queryString += ' WHERE CreatedDate > LAST_N_DAYS:30';

// Than this on a non-indexed field
queryString += ' WHERE CustomTextField__c = \'something\'';

Add LIMIT clauses

Always limit your results unless you really need everything:

queryString += ' LIMIT 1000'; // Or whatever makes sense

Consider using Database.query with Query Plan

You can analyze query performance in the Developer Console by looking at the query plan. If a query is slow, check if filters are using indexes.

Real-World Use Case: Custom Report Builder

Here’s a practical example—a simple report builder that lets users create custom Account reports:

public class CustomReportBuilder {

    public class ReportCriteria {
        public List<String> fields;
        public String industry;
        public String state;
        public Decimal minRevenue;
        public String sortField;
        public String sortOrder;
        public Integer limitRows;
    }

    public static List<Account> generateReport(ReportCriteria criteria) {

        // Validate and build field list
        Set<String> allowedFields = new Set<String>{
            'Id', 'Name', 'Industry', 'BillingState', 'AnnualRevenue', 
            'Phone', 'Website', 'CreatedDate', 'Owner.Name'
        };

        List<String> validFields = new List<String>();
        for(String field : criteria.fields) {
            if(allowedFields.contains(field)) {
                validFields.add(field);
            }
        }

        if(validFields.isEmpty()) {
            validFields.add('Id');
            validFields.add('Name');
        }

        // Build query
        String queryString = 'SELECT ' + String.join(validFields, ', ') + ' FROM Account';

        // Build WHERE conditions
        List<String> conditions = new List<String>();

        if(String.isNotBlank(criteria.industry)) {
            conditions.add('Industry = \'' + String.escapeSingleQuotes(criteria.industry) + '\'');
        }

        if(String.isNotBlank(criteria.state)) {
            conditions.add('BillingState = \'' + String.escapeSingleQuotes(criteria.state) + '\'');
        }

        if(criteria.minRevenue != null && criteria.minRevenue > 0) {
            conditions.add('AnnualRevenue >= ' + criteria.minRevenue);
        }

        if(!conditions.isEmpty()) {
            queryString += ' WHERE ' + String.join(conditions, ' AND ');
        }

        // Add sorting
        if(String.isNotBlank(criteria.sortField) && allowedFields.contains(criteria.sortField)) {
            queryString += ' ORDER BY ' + criteria.sortField;
            if(criteria.sortOrder == 'DESC') {
                queryString += ' DESC';
            } else {
                queryString += ' ASC';
            }
        }

        // Add limit
        Integer rowLimit = (criteria.limitRows != null && criteria.limitRows > 0) ? criteria.limitRows : 100;
        queryString += ' LIMIT ' + rowLimit;

        System.debug('Generated Query: ' + queryString);

        return Database.query(queryString);
    }
}

You’d call it like this:

CustomReportBuilder.ReportCriteria criteria = new CustomReportBuilder.ReportCriteria();
criteria.fields = new List<String>{'Name', 'Industry', 'AnnualRevenue'};
criteria.industry = 'Technology';
criteria.state = 'California';
criteria.minRevenue = 1000000;
criteria.sortField = 'AnnualRevenue';
criteria.sortOrder = 'DESC';
criteria.limitRows = 50;

List<Account> reportResults = CustomReportBuilder.generateReport(criteria);

This pattern gives you maximum flexibility while keeping security tight with whitelists.

SOQL Injection in Salesforce Apex

Dynamic SOQL Working with Custom Objects in Apex

Dynamic queries work exactly the same with custom objects:

String objectName = 'Custom_Object__c';
String queryString = 'SELECT Id, Name, Custom_Field__c FROM ' + objectName;
queryString += ' WHERE Custom_Field__c != null';
queryString += ' LIMIT 100';

List<sObject> records = Database.query(queryString);

for(sObject rec : records) {
    String name = (String)rec.get('Name');
    String customValue = (String)rec.get('Custom_Field__c');
    System.debug(name + ': ' + customValue);
}

Just remember to include the __c suffix for custom objects and fields.

Dynamic SOQL in Lightning Web Components

When you’re using LWCs, you typically call Apex methods that use dynamic queries:

Apex Controller:

public with sharing class AccountSearchController {
    
    @AuraEnabled(cacheable=true)
    public static List<Account> searchAccounts(String searchTerm, String industry) {
        
        String queryString = 'SELECT Id, Name, Industry, Phone FROM Account';
        List<String> conditions = new List<String>();
        
        if(String.isNotBlank(searchTerm)) {
            String safeTerm = String.escapeSingleQuotes(searchTerm);
            conditions.add('Name LIKE \'%' + safeTerm + '%\'');
        }
        
        if(String.isNotBlank(industry)) {
            conditions.add('Industry = \'' + String.escapeSingleQuotes(industry) + '\'');
        }
        
        if(!conditions.isEmpty()) {
            queryString += ' WHERE ' + String.join(conditions, ' AND ');
        }
        
        queryString += ' ORDER BY Name LIMIT 50';
        
        return Database.query(queryString);
    }
}

JavaScript in LWC:

import { LightningElement, track } from 'lwc';
import searchAccounts from '@salesforce/apex/AccountSearchController.searchAccounts';

export default class AccountSearch extends LightningElement {
    @track searchTerm = '';
    @track selectedIndustry = '';
    @track accounts = [];
    
    handleSearch() {
        searchAccounts({ 
            searchTerm: this.searchTerm, 
            industry: this.selectedIndustry 
        })
        .then(result => {
            this.accounts = result;
        })
        .catch(error => {
            console.error('Error:', error);
        });
    }
}

The dynamic query lets users search flexibly without redeploying code for every new requirement.

Working with Different sObject Types in String Queries in Apex

Dynamic Count Queries in Salesforce Apex

Sometimes you just need to know how many records match:

public static Integer getAccountCount(String industry) {
    String queryString = 'SELECT COUNT() FROM Account';
    
    if(String.isNotBlank(industry)) {
        queryString += ' WHERE Industry = \'' + String.escapeSingleQuotes(industry) + '\'';
    }
    
    Integer count = Database.countQuery(queryString);
    return count;
}

Note that Database.countQuery() returns an Integer directly, not a list.

Aggregate Queries with Dynamic SOQL in Salesforce Apex

You can build aggregate queries dynamically, too:

public class StringQuery {

    public static void runQuery() {

        List<AggregateResult> results = getRevenueByIndustry();

        for(AggregateResult ar : results) {
            String industry = (String)ar.get('Industry');
            Decimal revenue = (Decimal)ar.get('revenue');
            Integer count = (Integer)ar.get('accountCount');

            System.debug(industry + ': ' + revenue + ' (' + count + ' accounts)');
        }
    }

    public static List<AggregateResult> getRevenueByIndustry() {

        String queryString = 'SELECT Industry, SUM(AnnualRevenue) revenue, COUNT(Id) accountCount';
        queryString += ' FROM Account';
        queryString += ' WHERE Industry != null';
        queryString += ' GROUP BY Industry';
        queryString += ' ORDER BY SUM(AnnualRevenue) DESC';

        System.debug('Query: ' + queryString);

        List<AggregateResult> results = Database.query(queryString);

        return results;
    }
}

Aggregate results require casting values with the get() method since they return generic sObject types.

Aggregate Queries with Dynamic SOQL in Salesforce Apex

Dynamic SOQL with Date Literals in Salesforce Apex

Date filters are common and work smoothly with dynamic queries:

public static List<Account> getRecentAccounts(Integer days) {
    String queryString = 'SELECT Id, Name, CreatedDate FROM Account';
    queryString += ' WHERE CreatedDate = LAST_N_DAYS:' + days;
    queryString += ' ORDER BY CreatedDate DESC';
    
    return Database.query(queryString);
}

Or with actual date values:

public static List<Account> getAccountsByDateRange(Date startDate, Date endDate) {
    String queryString = 'SELECT Id, Name, CreatedDate FROM Account';
    queryString += ' WHERE CreatedDate >= ' + startDate.format();
    queryString += ' AND CreatedDate <= ' + endDate.format();
    
    return Database.query(queryString);
}
Dynamic SOQL with Date Literals in Salesforce Apex

Best Practices Summary

Let me wrap up with the key things to remember:

Security first

  • Always use String.escapeSingleQuotes() on user input
  • Whitelist field names and sort orders
  • Never trust data from external sources
  • Consider using Database.queryWithBinds() when possible

Keep it readable

  • Build queries in logical chunks
  • Use variables for complex conditions
  • Add comments explaining business logic
  • Debug your query strings before execution

Performance matters

  • Only select the fields you actually need
  • Always include LIMIT clauses
  • Use indexed fields in WHERE clauses
  • Test with realistic data volumes

Error handling

  • Wrap queries in try-catch blocks
  • Log failures for debugging
  • Provide meaningful error messages
  • Have fallback behavior when queries fail

Testing thoroughly

  • Test all filter combinations
  • Include edge cases and empty values
  • Test injection attempts
  • Verify governor limit scenarios

When to Avoid Dynamic SOQL

Before I close, let me be clear about when NOT to use dynamic queries:

  • Simple, unchanging queries: Use static SOQL—it’s faster and clearer
  • Queries in loops: Never build queries inside loops; it’s a governor limit disaster waiting to happen
  • When you know the fields at compile time, Static queries catch field errors at compile time
  • Complex nested logic: If your query builder has tons of if-else statements, rethink your approach

Conclusion

Dynamic SOQL with string queries gives you incredible flexibility in Apex. You can build search screens, custom reports, configurable batch jobs, and generic utility classes that would be impossible with static queries.

The key is balancing flexibility, security, and performance. Always sanitize inputs, validate field names, and test thoroughly.

Start simple—build one dynamic WHERE clause. Then add complexity as needed. Before long, you’ll be building sophisticated query engines that make your apps far more powerful and user-friendly.

You may like to read:

Agentforce in Salesforce

DOWNLOAD FREE AGENTFORCE EBOOK

Start with AgentForce in Salesforce. Create your first agent and deploy to your Salesforce Org.

Salesforce flows complete guide

FREE SALESFORCE FLOW EBOOK

Learn how to work with flows in Salesforce with 5 different real time examples.