Salesforce Interview Questions & Answers Book is now available!

Grab Now

Interview questions

SOQL and SOSL Interview Questions and Answers (2026)

SOQL and SOSL interview questions with answers: relationship queries, aggregates, selectivity and indexes, dynamic SOQL injection, USER_MODE and FOR UPDATE.

By Teja, ex-Salesforce engineer · Updated October 6, 2026 · 14 min read

TL;DR

  • SOQL questions go beyond syntax: interviewers want relationship queries, aggregates, selectivity, injection-safe dynamic SOQL and how you enforce the user's access.
  • Use WITH USER_MODE for new code. It enforces object permissions, field-level security and sharing, and it checks every clause, not just SELECT.
  • The limits that matter: 100 SOQL queries per synchronous transaction (200 async), 50,000 rows retrieved, 20 SOSL queries and 2,000 rows per SOSL query.

These are the SOQL and SOSL questions that come up in Salesforce developer interviews in 2026, with answers that explain why each rule exists. Queries sit inside almost every other interview topic, so weak answers here usually show up again in the Apex and trigger rounds.

The questions are grouped by level. Fundamentals covers syntax and relationships. Intermediate is where performance and security come in: selectivity, dynamic SOQL, WITH USER_MODE and FOR UPDATE. Scenario-based and Senior / design cover the limit traps and large data volumes. Write or say your answer before you read mine.

Fundamentals

When do you use SOQL, and when SOSL?

Use SOQL when you know which object you're querying and want exact filters: "open opportunities for these accounts, closing this quarter". It queries the database directly, supports relationships and aggregates, and returns a list of one object type.

Use SOSL when you're searching text and don't know which object or field the match is in: "find 'Acme' in accounts, contacts and leads". It uses the search index, can search many objects in one statement, and returns a List<List<SObject>> with one list per object.

List<List<SObject>> results = [
    FIND :searchTerm IN NAME FIELDS
    RETURNING Account(Id, Name), Contact(Id, Name, Email), Lead(Id, Name, Company)
    LIMIT 20
];
List<Account> accounts = (List<Account>) results[0];

Limits differ too: 20 SOSL queries per transaction, and up to 2,000 rows per SOSL query.

What the interviewer is really checking: that you pick by "exact filter" versus "text search", not by habit.

How do relationship queries work in both directions?

Child to parent uses dot notation through the lookup or master-detail field. For custom relationships, replace __c with __r:

List<Contact> contacts = [SELECT Id, Name, Account.Name, Account.Owner.Name FROM Contact WHERE AccountId IN :accountIds];
List<Invoice__c> invoices = [SELECT Id, Customer__r.Name FROM Invoice__c];

You can go up to five levels from the starting object.

Parent to child uses a subquery with the child relationship name, which is plural for standard objects (Contacts, Opportunities) and ends in __r for custom ones:

for (Account acc : [
    SELECT Id, Name, (SELECT Id, LastName FROM Contacts), (SELECT Id FROM Invoices__r)
    FROM Account WHERE Id IN :accountIds
]) {
    Integer contactCount = acc.Contacts.size();
}

The child relationship name is set on the lookup field, so check it in Object Manager rather than guessing. Rows returned by subqueries count toward the 50,000-row limit too.

What's wrong with assigning a query directly to a single record?

Account acc = [SELECT Id, Name FROM Account WHERE Name = :name];

If the query returns no rows, Apex throws System.QueryException: List has no rows for assignment to SObject. If it returns more than one row, it throws as well, because a single variable can't hold several records.

Query into a list and handle the cases explicitly:

List<Account> matches = [SELECT Id, Name FROM Account WHERE Name = :name LIMIT 1];
Account acc = matches.isEmpty() ? null : matches[0];

Direct assignment is fine when the query can only return one row and its absence really is an error, such as querying by an Id you just inserted. Anywhere the data comes from users or integrations, use the list.

What the interviewer is really checking: that you write code for the data you might get, not the data you expect.

What are semi-joins and anti-joins?

A semi-join filters records by whether related records exist, using IN with a subquery. An anti-join does the opposite, with NOT IN.

// Accounts that have at least one open case
List<Account> withOpenCases = [
    SELECT Id, Name FROM Account
    WHERE Id IN (SELECT AccountId FROM Case WHERE IsClosed = false)
];

// Accounts with no contacts at all
List<Account> withoutContacts = [
    SELECT Id, Name FROM Account
    WHERE Id NOT IN (SELECT AccountId FROM Contact)
];

They replace a two-step pattern of querying Ids into a set and then querying again, which costs two queries and puts every Id on the heap. The subquery must select a single Id or lookup field. "Accounts with no contacts" is a classic interview question, and the anti-join is the answer they're looking for.

How do aggregate queries work in Apex?

Aggregate functions such as COUNT(), COUNT(field), SUM(), AVG(), MIN() and MAX() return AggregateResult rows. Combine them with GROUP BY, and filter groups with HAVING. Give each aggregate an alias so you can read it back by name. Without one, the columns are named expr0, expr1 and so on.

for (AggregateResult row : [
    SELECT AccountId, COUNT(Id) openCount, SUM(Amount) pipeline
    FROM Opportunity
    WHERE IsClosed = false AND AccountId != null
    GROUP BY AccountId
    HAVING COUNT(Id) > 5
]) {
    Id accountId = (Id) row.get('AccountId');
    Integer openCount = (Integer) row.get('openCount');
    Decimal pipeline = (Decimal) row.get('pipeline');
}

HAVING filters on the aggregated values, WHERE filters rows before grouping. Mixing them up is the most common mistake. And aggregates aren't a loophole for the row limit: for functions such as SUM() and AVG(), every row the aggregation reads counts toward the 50,000 rows.

How do you paginate query results, and what's the catch with OFFSET?

LIMIT and OFFSET give you simple pages:

List<Contact> page = [SELECT Id, Name FROM Contact ORDER BY Name, Id LIMIT 50 OFFSET :offsetValue];

The catch is that OFFSET can't go beyond 2,000 rows, so with 50-row pages, page 42 fails.

For deep pagination, use keyset pagination: remember the last value you returned and ask for what comes after it.

List<Contact> nextPage = [SELECT Id, Name FROM Contact WHERE Id > :lastSeenId ORDER BY Id LIMIT 50];

Always use a stable ORDER BY, and include Id as a tie-breaker, or rows can repeat or disappear between pages. For very large exports in Apex, Batch Apex with a query locator is the tool, not pagination.

Intermediate

What makes a query selective, and which fields are indexed?

A query is selective when an indexed field in the WHERE clause narrows the results enough for the optimizer to use the index instead of scanning the table. That matters on large objects, where a full scan is slow or, in some contexts, refused.

Fields indexed by default include Id, Name, OwnerId, CreatedDate, SystemModstamp, RecordTypeId, lookup and master-detail fields, and fields marked External ID or Unique. Salesforce Support can add custom indexes to other fields.

Salesforce's documented thresholds: a standard index is used when the filter matches fewer than 30% of the first million records and 15% of the rest, up to 1 million records. A custom index needs fewer than 10% of the first million and 5% of the rest, up to 333,333 records.

What usually kills selectivity:

  • negative filters: !=, NOT IN, NOT LIKE, EXCLUDES,
  • a leading wildcard: LIKE '%corp',
  • filtering on null for a custom indexed field, because custom indexes don't include nulls by default,
  • non-deterministic formula fields.

What the interviewer is really checking: that you can explain why a query is slow, not just that it is.

How do you write dynamic SOQL that's safe from injection?

Never concatenate user input into a query string. Use bind variables, and since you can't bind a field name, check field names against a list you trust.

public with sharing class AccountSearchController {
    private static final Set<String> SORTABLE_FIELDS = new Set<String>{ 'Name', 'CreatedDate', 'AnnualRevenue' };

    @AuraEnabled(cacheable=true)
    public static List<Account> search(String term, String sortField) {
        String orderBy = SORTABLE_FIELDS.contains(sortField) ? sortField : 'Name';
        String query = 'SELECT Id, Name, Industry FROM Account WHERE Name LIKE :pattern ORDER BY ' + orderBy + ' LIMIT 50';
        Map<String, Object> binds = new Map<String, Object>{ 'pattern' => '%' + term + '%' };
        return Database.queryWithBinds(query, binds, AccessLevel.USER_MODE);
    }
}

Database.queryWithBinds takes the bind values as a map, so they never become part of the query text, and the AccessLevel argument runs it in user mode. String.escapeSingleQuotes() is the fallback for older code that has to concatenate a value, but binding is the better answer.

The attack you're preventing: input like x' OR Name != ' that rewrites the WHERE clause and returns records the user shouldn't see.

What's the difference between WITH USER_MODE, WITH SECURITY_ENFORCED and stripInaccessible?

Apex queries run in system mode by default and ignore the user's object and field permissions. These are the three ways to change that:

WITH USER_MODEWITH SECURITY_ENFORCEDSecurity.stripInaccessible
Object and field permissionsYesYesYes
Sharing (record access)YesNo, depends on the class keywordNo, depends on the class keyword
Clauses checkedAll, including WHERESELECT and FROM onlyFields present on the records
On a violationThrows a QueryExceptionThrows on the first problemRemoves the fields silently
List<Contact> contacts = [SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId WITH USER_MODE];

Use WITH USER_MODE for new code. Salesforce recommends it over WITH SECURITY_ENFORCED, and a QueryException thrown by a user-mode query can report all the inaccessible fields, via getInaccessibleFields(). Use stripInaccessible when you'd rather return a partial record than fail.

What does FOR UPDATE do, and when do you need it?

FOR UPDATE locks the returned records for the rest of the transaction. Other transactions that try to lock or update them wait until yours commits or rolls back, and fail if they wait too long.

You need it when two transactions could read the same value and both write back a change based on it. The classic case is a counter:

Invoice_Counter__c counter = [
    SELECT Id, Next_Number__c FROM Invoice_Counter__c
    WHERE Name = 'Default' LIMIT 1 FOR UPDATE
];
Integer invoiceNumber = counter.Next_Number__c.intValue();
counter.Next_Number__c = invoiceNumber + 1;
update counter;

Without the lock, two users saving at the same moment could both read 1041 and both issue invoice 1041.

Rules worth knowing: you can't use ORDER BY in a query that uses FOR UPDATE, and locks last until the transaction ends, so lock as few records as possible and keep the transaction short. Too many locked rows is a common cause of UNABLE_TO_LOCK_ROW errors.

Why use a SOQL for loop instead of assigning a query to a list?

A list query loads every row into memory at once. A SOQL for loop retrieves records in batches behind the scenes, so you never hold the whole result on the heap.

// Loads everything into the heap first
List<Contact> all = [SELECT Id, Description FROM Contact WHERE MailingCountry = 'India'];

// Processes 200 records at a time
for (List<Contact> chunk : [SELECT Id, Description FROM Contact WHERE MailingCountry = 'India']) {
    ContactCleanup.normalise(chunk);
}

The list form of the loop, iterating List<Contact> rather than single records, hands you 200 records at a time, which suits bulk processing.

It saves heap, not query rows: every record still counts toward the 50,000-row limit. And if you do DML inside the loop, that's one DML statement per chunk, which is fine for a few thousand rows and a problem beyond that. For truly large volumes, Batch Apex is the answer.

How do you query a polymorphic field such as Task.WhatId?

A polymorphic lookup can point to several object types. WhatId on Task can reference an Account, an Opportunity, a custom object and more, and WhoId can reference a Contact or a Lead.

Filter by the type of the related record with Type:

List<Task> oppTasks = [
    SELECT Id, Subject, What.Name
    FROM Task
    WHERE What.Type = 'Opportunity' AND IsClosed = false
];

Through the polymorphic relationship you can only reach a limited set of common fields, such as Name. For type-specific fields, such as an Opportunity's StageName, collect the WhatId values and query the target object directly:

Set<Id> opportunityIds = new Set<Id>();
for (Task t : oppTasks) {
    opportunityIds.add(t.WhatId);
}
Map<Id, Opportunity> opportunities = new Map<Id, Opportunity>(
    [SELECT Id, StageName FROM Opportunity WHERE Id IN :opportunityIds]
);

How do you query deleted records?

Add ALL ROWS in Apex. It returns records in the recycle bin as well as active ones, and archived activities too. Filter on IsDeleted to get only the deleted ones:

List<Account> deleted = [SELECT Id, Name FROM Account WHERE IsDeleted = true ALL ROWS];

Typical uses are building an undelete feature, auditing deletions, or making sure an integration sync sees deletes.

Two things to mention. ALL ROWS is Apex-only syntax. Through the APIs, you use the queryAll operation instead. And records only stay in the recycle bin for a limited time, then they're gone for good, so a sync that depends on catching deletes should run regularly rather than rely on the bin.

Scenario-based

You get "Too many query rows: 50001". How do you fix it?

One transaction tried to retrieve more than 50,000 records across all its queries. The fix depends on why it needs that many:

  • It doesn't really need them. Add filters, use LIMIT, or select only what you'll use. Often a query is missing a date range or a status filter.
  • It only needs a total. Use an aggregate, or store a rollup on the parent, instead of fetching rows to count them in Apex. Measure the result with Limits.getQueryRows() in your org, because aggregates don't make large volumes free.
  • It needs to process every row. Move it to Batch Apex. The start method's query locator can cover up to 50 million records, and each execute gets a fresh limit.
  • Subqueries are bigger than expected. Child rows count too. A query for 1,000 accounts with their contacts can easily pass 50,000.

What the interviewer is really checking: that your first question is "does this transaction really need all those rows?"

This code queries inside a loop. Fix it.

for (Opportunity opp : opportunities) {
    Account acc = [SELECT Id, Rating FROM Account WHERE Id = :opp.AccountId];
    opp.Account_Rating__c = acc.Rating;
}

It runs one query per opportunity, so 101 opportunities hit the 100-query limit. It also throws if an opportunity has no account. The fix is the collect, query once, map pattern:

Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : opportunities) {
    accountIds.add(opp.AccountId);
}
accountIds.remove(null);

Map<Id, Account> accounts = new Map<Id, Account>(
    [SELECT Id, Rating FROM Account WHERE Id IN :accountIds]
);

for (Opportunity opp : opportunities) {
    Account acc = accounts.get(opp.AccountId);
    if (acc != null) {
        opp.Account_Rating__c = acc.Rating;
    }
}

One query, whatever the number of opportunities. If the opportunities were queried in the first place, an even better answer is to fetch Account.Rating in that original query with dot notation and skip the second query entirely.

Get each account with only its most recent opportunity.

Use a parent-to-child subquery with its own ORDER BY and LIMIT:

for (Account acc : [
    SELECT Id, Name,
        (SELECT Id, Name, CloseDate, Amount FROM Opportunities ORDER BY CloseDate DESC LIMIT 1)
    FROM Account
    WHERE Id IN :accountIds
]) {
    Opportunity latest = acc.Opportunities.isEmpty() ? null : acc.Opportunities[0];
}

The LIMIT 1 applies per account, not to the whole query, which is what makes this work. Handle accounts with no opportunities, because the subquery returns an empty list for them.

A follow-up I like: when a parent has a large number of child records, reading the child list directly can throw "Aggregate query has too many rows for direct assignment, use FOR loop". The fix is to iterate the children with a nested for loop instead of assigning or indexing the whole list.

Build a search box that finds records across accounts, contacts and leads.

This is SOSL territory: the user types text, and you don't know which object or field matches.

public with sharing class GlobalSearchController {
    @AuraEnabled(cacheable=true)
    public static List<List<SObject>> search(String term) {
        if (String.isBlank(term) || term.trim().length() < 2) {
            return new List<List<SObject>>();
        }
        return [
            FIND :term IN NAME FIELDS
            RETURNING Account(Id, Name LIMIT 5), Contact(Id, Name, Email LIMIT 5), Lead(Id, Name, Company LIMIT 5)
        ];
    }
}

Points to mention: a per-object LIMIT inside RETURNING keeps one object from crowding out the others, and binding :term keeps the input out of the query text. The search index updates asynchronously, so a record created a moment ago may not appear yet. And in test classes, SOSL returns nothing unless you set the results with Test.setFixedSearchResults().

A query returns records for an admin but nothing for a sales user. How do you debug it?

Work through the access layers in order:

  1. Sharing. Is the class with sharing, or is the query WITH USER_MODE? Then the user only sees records they own or that are shared with them. Check the org-wide default, role hierarchy and sharing rules for the object.
  2. Object and field permissions. With WITH USER_MODE, a missing permission on any field in the query, including fields used only in WHERE, throws an exception instead of returning rows. Check that the error isn't being caught and swallowed somewhere.
  3. The filter itself. Is it filtering on OwnerId = :UserInfo.getUserId(), a record type the user's profile doesn't use, or a field the user's records don't populate?

To reproduce it, log in as the user from Setup (if your org lets administrators log in as any user), or write a test that runs the query inside System.runAs() with a user who has the same profile and permission sets.

What the interviewer is really checking: that you know which layer controls rows and which controls fields.

A trigger fails with "Non-selective query against large object type". What do you do?

In a trigger, a query against an object with more than 200,000 records must be selective, or Salesforce refuses to run it rather than let a full table scan slow down a save. The error usually appears in production first, because sandboxes have less data.

Steps:

  1. Find the filter. Look at the query's WHERE clause. Is it filtering only on unindexed fields, using != or NOT IN, using a leading wildcard, or comparing a custom field to null?
  2. Add an indexed filter. Often the trigger already knows the Ids or a lookup value, so filter on Id IN :ids or the lookup field instead.
  3. Narrow the data. A date range on CreatedDate, or a status filter, can bring it under the threshold.
  4. Request a custom index from Salesforce Support for a field you filter on often, or mark it as an External ID if it really is one.
  5. Check with the Query Plan tool in the Developer Console before and after.

Senior / design

How do you read a query plan?

In the Developer Console, enable the Query Plan tool in preferences, then paste your query into the Query Editor and click Query Plan. It lists the plans the optimizer considered, in order of preference, with:

  • Leading operation type: Index, TableScan, Sharing or Other. A TableScan on a large object is a warning sign.
  • Cardinality: how many rows the optimizer expects that plan to return.
  • sObject cardinality: how many rows the object has.
  • Cost: the optimizer's relative cost. A cost below 1 means the plan is selective.

The plan at the top is the one it will use. If that's a table scan, look at the other candidates. They show which index the optimizer considered and how far it was from the threshold, which tells you whether a tighter filter or a custom index would help.

The REST API has an explain parameter on the query resource that returns the same information, which is useful in scripts.

What problems do large data volumes and data skew cause?

Large data volumes make non-selective queries slow or refused, slow down reports and list views, and make sharing recalculations take longer. The tools are selective queries, custom indexes, skinny tables (enabled by Salesforce Support for specific objects and fields), archiving old data, and Batch Apex or the Bulk API for processing.

Data skew is when too many records concentrate on one point:

  • Account data skew: a single account with a very large number of child records, more than 10,000 is Salesforce's guideline. Updating many of those children at once causes UNABLE_TO_LOCK_ROW errors, because each update locks the parent, and sharing changes on the parent take a long time to recalculate.
  • Ownership skew: one user owning a very large number of records of an object, often an integration user. Changing that user's role triggers expensive sharing recalculations.
  • Lookup skew: a very large number of records pointing to the same lookup value, which causes lock contention on inserts and updates.

The fixes are design decisions: spread children across several parent records, keep skewed owners out of the role hierarchy or at the top of it, and make lookups more specific.

What the interviewer is really checking: that you'd find this in design, not in a production incident.

How do you design queries for an object with tens of millions of records?

Start from the access pattern, not the query:

  1. Know what every frequent query filters on, and make sure each filter uses an indexed field and stays under the selectivity threshold. Plan custom indexes with Salesforce Support early.
  2. Keep queries narrow: only the fields you need, always with a selective WHERE clause and a LIMIT where the UI only shows a page.
  3. Avoid query patterns that can't use indexes: negative filters, leading wildcards, null comparisons on custom fields and non-deterministic formulas.
  4. Process in bulk asynchronously: Batch Apex with a query locator, or the Bulk API for integrations, rather than large synchronous queries.
  5. Archive. Data you don't query day to day can move to Big Objects or an external store, which keeps the main object smaller.
  6. Test at volume. A full or partial copy sandbox shows plans that a developer sandbox with a few hundred records never will.

The honest senior answer is that most query problems at this scale are data model problems, and the time to solve them is before the data arrives.

How to practise these SOQL questions out loud

SOQL is easy to read and surprisingly hard to write from memory. Here's how I'd practise:

  1. Write the query before you read the answer. On paper or in a plain text editor, without autocomplete.
  2. Run every query. Use the Developer Console Query Editor or the Salesforce CLI against a Developer Edition org, and fix what fails. Relationship names are where most people trip.
  3. Explain the why out loud. For each query, say why it's selective, how many rows it can return, and whose permissions it runs with.
  4. Reproduce the traps: the "List has no rows" exception, a semi-join, the OFFSET limit, and a SOSL test that returns nothing without Test.setFixedSearchResults().
  5. Use the Query Plan tool on a few of your own queries until reading one feels normal.

Salesforce Development (Visualforce & Apex) has dedicated SOQL, SOSL and DML modules, all built live in a real org, and the Lifetime plan includes the 700+ page Interview Questions Book if you want more questions to practise with.

Want 700+ pages of this?

The Salesforce Interview Questions & Answers Book goes far deeper than this page — it's included with the Lifetime plan, alongside the Real-Time Projects series.

Frequently asked questions

Do interviewers ask SOQL questions on their own?

Usually they're mixed into Apex and trigger rounds, but some interviews include a short query-writing exercise against a described data model. Expect to write a relationship query or an aggregate by hand.

Will I be asked to write queries without running them?

Often. Practise writing parent-to-child subqueries, semi-joins and GROUP BY queries on paper, then check them in the Developer Console Query Editor or with the Salesforce CLI.

How deep do SOQL performance questions go?

For mid-level roles, expect selectivity, indexed fields and why a leading wildcard is slow. Senior and architect roles add large data volumes, data skew and reading a query plan.

Is SOSL still asked in interviews?

Less than SOQL, but a question on when to choose SOSL over SOQL is common, often as part of building a search feature.

Keep going

Keep me in the loop!

Email me when a new episode is published (promise, I will not spam you)

You're signing up to receive emails from Salesforce Casts