Skip to main content

SF-0286 · Concept · Medium

What is dynamic SOQL?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

Dynamic SOQL is a SOQL statement constructed as a string at runtime and executed with Database.query() (or its safer cousin Database.queryWithBinds()). Static, inline SOQL — [SELECT Id FROM Account] — is compiled and bound at build time. Dynamic SOQL is bound at runtime, which means you can decide the object, fields, filters, and limits based on what the user, configuration, or metadata tells you.

When to reach for it

Use dynamic SOQL only when static SOQL truly can’t do the job:

  • The object name isn’t known at compile time (generic utilities, metadata-driven controllers).
  • The set of fields is configured in Custom Metadata.
  • The filter clause changes based on the user’s selection — search builders, advanced filters in LWC.
  • You’re building a package that runs against custom objects you don’t ship with.

For everything else, static SOQL wins: it’s compile-checked, the platform won’t let you deploy a query that references a missing field, and there’s no injection risk.

The two execution methods

// Old style — works but vulnerable if you concatenate untrusted input
String soql = 'SELECT Id, Name FROM Account WHERE Name LIKE \'%' + search + '%\'';
List<SObject> results = Database.query(soql);

// Modern style — bind variables prevent SOQL injection
String soql = 'SELECT Id, Name FROM Account WHERE Name LIKE :search';
Map<String, Object> binds = new Map<String, Object>{
    'search' => '%' + search + '%'
};
List<SObject> results = Database.queryWithBinds(soql, binds, AccessLevel.USER_MODE);

Database.queryWithBinds was introduced in Spring ‘23. It accepts a map of named bind variables and an AccessLevel — either USER_MODE (enforce running user’s FLS and sharing) or SYSTEM_MODE (bypass — admin-only).

SOQL injection: the must-know risk

This is the canonical bug interviewers love to ask about. If a user can type into a search box and your code does:

String soql = 'SELECT Id, Name FROM Contact WHERE LastName = \'' + lastName + '\'';

a malicious input like ' OR Id != null OR LastName = ' returns every contact in the org. Two defenses:

  1. Bind variables:lastName is parsed as a value, never as SQL.
  2. String.escapeSingleQuotes(input) — when bind variables aren’t an option (such as a field or object name), at minimum escape single quotes before concatenation.
String safe = String.escapeSingleQuotes(lastName);
String soql = 'SELECT Id, Name FROM Contact WHERE LastName = \'' + safe + '\'';
public static List<SObject> search(String objectName,
                                   List<String> fields,
                                   String searchTerm) {
    String fieldList = String.join(fields, ', ');
    String soql = 'SELECT ' + fieldList +
                  ' FROM '   + objectName +
                  ' WHERE Name LIKE :term' +
                  ' WITH USER_MODE' +
                  ' LIMIT 200';

    return Database.queryWithBinds(
        soql,
        new Map<String, Object>{ 'term' => '%' + searchTerm + '%' },
        AccessLevel.USER_MODE
    );
}

Two things to call out:

  • WITH USER_MODE (Winter ‘23+) enforces the running user’s FLS and sharing on the query — the modern replacement for WITH SECURITY_ENFORCED. Without it, the query runs in system mode.
  • objectName and fieldList are not bind-able — they’re SOQL syntax. If they come from user input, validate against Schema.getGlobalDescribe() to make sure they’re real objects/fields the user can access.

Governor-limit angle

Dynamic SOQL counts against the same 100 SOQL queries / 50,000 rows / LIMIT rules as static SOQL. There’s no extra cost — but there’s also no extra budget. Common gotcha: dynamic SOQL inside a for loop hits the SOQL-101 limit just as fast as static SOQL.

Static SOQL vs dynamic SOQL — when to choose which

AspectStatic SOQLDynamic SOQL
Compile-time field checkYesNo
Bind variables:variable inlineDatabase.queryWithBinds() map
Injection riskNoneYes if concatenating
Object/field name flexibilityNoneFull
ReadabilityHighLower

What interviewers are really looking for

Naming Database.query() is the floor. Real signal: (1) you reach for static SOQL first and only escalate to dynamic when needed, (2) you know Database.queryWithBinds() is the modern, injection-safe API, (3) you mention WITH USER_MODE for FLS enforcement, (4) you can name String.escapeSingleQuotes() as the last-resort defense when bind variables don’t apply (object/field names).

Verified against: SOQL/SOSL Reference — Dynamic SOQL, Apex Reference — Database.queryWithBinds. Last reviewed 2026-05-17.