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

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.

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.

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.

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

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.

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);
}
}
}
Looks innocent, right? But what if a user types this:
test' OR '1'='1Your 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');
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.

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 WHEREwithout 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);
}
}
}
}
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.

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 senseConsider 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.

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:
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);
}
}
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.

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.

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);
}

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:
- SOQL Governor Limits in Salesforce
- IN Operator/Clause in SOQL
- Salesforce SOQL toLabel() Function
- Query Records Created Today or Yesterday with SOQL in Salesforce
- Salesforce SOQL and SOSL Limits for Search Queries
I am Bijay Kumar, the founder of SalesforceFAQs.com. Having over 10 years of experience working in salesforce technologies for clients across the world (Canada, Australia, United States, United Kingdom, New Zealand, etc.). I am a certified salesforce administrator and expert with experience in developing salesforce applications and projects. My goal is to make it easy for people to learn and use salesforce technologies by providing simple and easy-to-understand solutions. Check out the complete profile on About us.