SOQL Multi-Select Picklist Queries
This skill activates when a SOQL query has to filter on a multi-select picklist field. A multi-select picklist stores every selected value in one semicolon-delimited string, so ordinary equality is fragile and the query language exposes dedicated containment operators — INCLUDES and EXCLUDES — with their own semicolon-AND / comma-OR grammar. It also carries a hard restriction: a multi-select picklist may not appear in ORDER BY.
The SOQL reference documents this as standard WHERE-clause syntax and stamps no GA/Beta/Pilot label on it. The only version gate is that querying a value by its API name (which can differ from the display label) is available in API version 39.0 and later.
Before Starting
Gather this context before writing the query:
- Confirm the field is actually a multi-select picklist. In Setup the field type reads Picklist (Multi-Select); in metadata the field's
<type> is MultiselectPicklist. If it is an ordinary single-select picklist, none of this applies — use plain = or IN. Guessing wrong is the most common source of "why doesn't my filter work."
- Decide AND vs OR per group. For each value or group of values, know whether the record must have all of them selected (AND) or any of them (OR). That decision maps directly to semicolons and commas — get it explicit before you type.
- Know whether you are matching the label or the API name. A value's API name can differ from what users see. Filtering by API name is only supported in API version 39.0+; below that you match the display value.
- Know where the query runs. Static SOQL can bind a value directly; dynamic SOQL built by string concatenation is a SOQL-injection surface (see the Security pillar in
references/well-architected.md).
Core Concepts
The four operators
A multi-select picklist supports exactly four comparison operators in a WHERE clause:
| Operator | Meaning (per the SOQL reference) |
|---|
= | Equals the specified string |
!= | Does not equal the specified string |
INCLUDES | Contains the specified string |
EXCLUDES | Does not contain the specified string |
INCLUDES and EXCLUDES are containment tests — they are what you almost always want. = and != compare against the entire stored string.
Semicolon = AND, comma = OR
The grammar lives inside the quoted operands:
- A semicolon joins values that must all be selected on the same record.
'AAA;BBB' means "AAA and BBB are both selected."
- A comma separates alternative operands inside
INCLUDES(...) / EXCLUDES(...) and expresses OR across those groups.
SELECT Id, MSP1__c FROM CustObj__c WHERE MSP1__c INCLUDES ('AAA;BBB','CCC')
The official worked example resolves like this. INCLUDES ('AAA;BBB','CCC') matches a stored value of 'AAA;BBB' or 'AAA;BBB;DDD' (they satisfy the first group), and also 'CCC', 'CCC;EEE', or 'AAA;CCC' (they satisfy the second group). A record whose only selection is 'AAA' matches neither group.
= is a whole-string exact match, not a containment test
The reference example WHERE MSP1__c = 'AAA;BBB' matches only records whose stored value is exactly the string AAA;BBB. Because the stored value is one semicolon-delimited string, = is brittle: a record that also has DDD selected (stored as AAA;BBB;DDD), or that serialized the same two values in a different order, will not match. Reach for = / != only when you truly need an exact-selection match; use INCLUDES / EXCLUDES for "has this value."
Label vs API name (API 39.0+)
Since API version 39.0, you can filter by a picklist value's API name, which can differ from the actual (display) value. Below 39.0 you filter by the display value. This matters when labels have been translated or renamed while the underlying API names stayed stable.
Multi-select picklist is not sortable
A multi-select picklist is one of the data types that cannot appear in an ORDER BY clause (alongside rich text area, long text area, encrypted fields, and data category group reference). Attempting it is a query error, not a silent no-op. Sort on a different field (or sort in Apex after the query).
Common Patterns
Containment — "has this value" (INCLUDES)
When to use: the everyday case — find records where a value (or a required set of values) is selected, regardless of what else is selected.
How it works: put each required-together set in one quoted operand with semicolons, and OR alternative sets with commas.
SELECT Id FROM Contact WHERE Interests__c INCLUDES ('Golf','Tennis;Squash')
Why not the alternative: Interests__c = 'Golf' matches only contacts whose sole selection is Golf; LIKE '%Golf%' looks tempting but risks matching substrings of other values (e.g. Golfing) and reads across the whole delimited blob unpredictably.
Exclusion — "does not have this value" (EXCLUDES)
When to use: filter records out by selection.
How it works: EXCLUDES is the negative of INCLUDES with the same semicolon/comma grammar.
SELECT Id FROM Contact WHERE Interests__c EXCLUDES ('Spam','Bulk;Promo')
Why not the alternative: != 'Spam' only excludes records whose entire selection is exactly Spam; someone with Spam;News would slip through.
Injection-safe filtering in Apex
When to use: the values come from user input, a Flow, an LWC, or any run-time source.
How it works: prefer a static SOQL query with a bind variable — the SOQL reference lists filter literals in WHERE clauses as a supported bind position, and demonstrates a bind used with INCLUDES. Build the semicolon/comma grouping in the String you bind:
String group1 = 'Tennis;Squash'; // both must be selected
String group2 = 'Golf'; // OR this one
// Static SOQL, bound literals — no string concatenation into the query:
List<Contact> matches = [
SELECT Id FROM Contact
WHERE Interests__c INCLUDES (:group1, :group2)
];
If you must build the clause dynamically, do not concatenate raw input into the query string; escape it (String.escapeSingleQuotes) or, better, keep the values in bind variables. See apex/apex-dynamic-soql-binding-safety.
Why not the alternative: concatenating user-supplied values straight into a Database.query(...) string is the classic SOQL-injection hole, and the semicolon grammar makes hand-built strings easy to get subtly wrong.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|
| "Record has value X selected" (X may be one of several selections) | INCLUDES ('X') | Containment test; ignores other selections |
| "Record has X and Y both selected" | INCLUDES ('X;Y') | Semicolon = AND inside one operand |
| "Record has X or Y selected" | INCLUDES ('X','Y') | Comma = OR across operands |
| "Record's selection is exactly X and nothing else" | = 'X' | Whole-string exact match is what you want here |
| "Record does not have X selected" | EXCLUDES ('X') | Negative containment; != 'X' only excludes the exact-X case |
| Values come from user/Flow/LWC input | Static SOQL with :bindVar, grouping built in the String | Injection-safe; filter literals are a supported bind position |
| Need to sort results by the multi-select field | Not possible in ORDER BY — sort on another field or in Apex | Multi-select picklist is unsupported in ORDER BY |
| Labels were translated/renamed | Filter by the value's API name (API 39.0+) | API name is stable; display label may differ |
Recommended Workflow
- Confirm the field type. Verify in Setup or metadata that the field is
MultiselectPicklist. If it is single-select, stop — use = / IN instead.
- Translate the requirement into groups. Write out each value set and label it AND (semicolon-joined) or OR (separate operand). This is the step people skip and then debug.
- Choose the operator.
INCLUDES for "has value(s)", EXCLUDES for "does not have", and = / != only for exact whole-selection matches.
- Decide label vs API name. If matching by API name, confirm the query runs at API version 39.0 or later.
- Make it injection-safe. In Apex, put run-time values in bind variables (build the semicolon/comma grouping inside the bound String); never concatenate raw input into a dynamic query.
- Keep the field out of ORDER BY. If results need sorting, order by a different field or sort the returned list in Apex.
- Validate. Run
scripts/check_soql_multiselect_picklist_queries.py over the source to catch =/!=/LIKE/ORDER BY on the multi-select field and missing-quote grouping bugs.
Review Checklist
Run through these before marking the query done:
Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
= silently under-matches — Field__c = 'AAA' matches only records whose entire selection is AAA; anyone who also picked another value is excluded with no error. This masquerades as "the filter works" on demo data (one value selected) and breaks in production. Use INCLUDES.
ORDER BY on the field is a hard error — multi-select picklist is an unsupported ORDER BY data type. The query fails to compile/run; it does not fall back to unsorted.
- Semicolon vs comma is easy to invert —
INCLUDES ('AAA,BBB') (comma inside the quotes) and INCLUDES ('AAA;BBB') (semicolon) mean different things, and a comma placed inside a single operand is treated as part of the literal, not as an OR separator. OR must be between quoted operands.
- API name ≠ label — filtering by the display label works, but if labels were translated or edited, matching by the value's API name (API 39.0+) is the stable choice; below 39.0 you have only the display value.
LIKE is a trap — LIKE '%Golf%' seems like a containment test but matches substrings across the delimited blob (e.g. Golfing, or a value that merely contains the letters) and is not the supported containment operator. Use INCLUDES.
Output Artifacts
| Artifact | Description |
|---|
SOQL WHERE clause | INCLUDES / EXCLUDES (or exact =) filter with correct semicolon-AND / comma-OR grouping |
| Injection-safe Apex query | Static SOQL binding the grouped value String(s), or an escaped dynamic query |
templates/soql-multiselect-picklist-queries-template.md | Fill-in worksheet + operator/grammar cheat-sheet that turns a requirement into the right clause |
scripts/check_soql_multiselect_picklist_queries.py | Stdlib validator that flags fragile =/!=/LIKE/ORDER BY and missing-quote grouping on multi-select fields |
Related Skills
apex/apex-dynamic-soql-binding-safety — how to bind values (including into INCLUDES) and avoid SOQL injection when the query is built dynamically.
apex/soql-null-ordering-patterns — the broader ORDER BY semantics; relevant because the multi-select field is excluded from sorting.
admin/picklist-and-value-sets — defining and maintaining the multi-select field, its values, and the label-vs-API-name distinction this skill filters on.
data/soql-query-optimization — selectivity and performance; multi-select picklist filters are not selective, so pair this skill with optimization work on large objects.