Apex SOQL 与 SOSL 查询完全指南

Apex SOQL与SOSL查询完全指南:覆盖SOQL语句(列表/单条/COUNT)、SOSL跨对象搜索(Apex与API语法差异)、查询结果处理(字段选择/解引用规则)、通过关系访问sObject字段(读取vs修改null行为/外部ID关联)、外键与父子关系查询、聚合函数(SUM/MAX/AVG/COUNT/GROUP BY/AggregateResult)、大查询处理(SOQL for循环vs堆大小限制)、查询选择性与自定义索引(默认索引字段/10%阈值/负运算符)、单条记录查询、过滤null性能优化、多态关系(Type/TYPEOF/instanceof)、绑定变量(:语法/FIND/WHERE/IN/LIMIT/OFFSET)、ALL ROWS查询(含回收站记录)、SOQL for循环双格式详解(单条vs列表200条批次)及性能考量。...

📅 2026/7/16 ✍️ ponybai 🏷️ apex, salesforce

SOQL 与 SOSL 查询

SOQL与SOSL

你可以在 Apex 中通过将语句包围在方括号中来即时评估 Salesforce 对象查询语言(SOQL)或 Salesforce 对象搜索语言(SOSL)语句。

SOQL 语句

SOQL

SOQL 语句求值结果为 sObject 列表、单个 sObject 或用于 count 方法查询的 Integer。

// 查询列表
List<Account> aa = [SELECT Id, Name FROM Account WHERE Name = 'Acme' WITH USER_MODE];

// 从现有对象创建新对象
Contact c = new Contact(Account = [SELECT Name FROM Account
    WHERE NumberOfEmployees > 10 WITH USER_MODE LIMIT 1]);
c.FirstName = 'James'; c.LastName = 'Yoyce';

// COUNT 查询
Integer i = [SELECT COUNT() FROM Contact WHERE LastName = 'Weissman' WITH USER_MODE];

// 算术运算
Integer j = 5 * [SELECT COUNT() FROM Account WITH USER_MODE];

SOSL 语句

SOSL

SOSL 语句求值结果为sObject 列表的列表,每个列表包含特定 sObject 类型的搜索结果。结果列表始终以 SOSL 查询中指定的相同顺序返回。如果 SOSL 查询对某个 sObject 类型没有返回任何记录,则搜索结果中包含该类型的一个空列表。

List<List<sObject>> searchList = [FIND 'map*' IN ALL FIELDS
    RETURNING Account (Id, Name), Contact, Opportunity, Lead WITH USER_MODE];

// 为每个返回对象创建数组
Account[] accounts = ((List<Account>)searchList[0]);
Contact[] contacts = ((List<Contact>)searchList[1]);
Opportunity[] opportunities = ((List<Opportunity>)searchList[2]);
Lead[] leads = ((List<Lead>)searchList[3]);

Apex 中 FIND 子句与 API 的语法差异:Apex 中 FIND 值用单引号分隔(如 'map*'),API 中用花括号分隔(如 {map*})。以系统模式运行的 Apex 在使用 IN ALL FIELDS 扫描匹配时忽略字段级安全。

处理 SOQL 和 SOSL 查询结果

查询结果

SOQL 和 SOSL 查询只返回原始查询中选定的 sObject 字段数据。如果你尝试访问未在查询中选定的字段(ID 除外),会收到运行时错误。

// 运行时错误——Name 未被选定
String name = [SELECT Id FROM Account WHERE Name = 'Singha' LIMIT 1].Name;

// 正确——Name 已包含在选定字段中
String name = [SELECT Id, Name FROM Account WHERE Name = 'Singha' LIMIT 1].Name;

始终需要解引用字段:即使只选定一个字段,SOQL/SOSL 查询也返回完整记录。你必须解引用才能访问它。唯一的例外是 COUNT 操作返回 Integer。公式字段返回查询时字段的值——对其他字段的更改不会反映在公式字段值中,直到记录被保存并重新查询。

// 必须解引用
Double rev = [SELECT AnnualRevenue FROM Account WHERE Name = 'Acme'][0].AnnualRevenue;

// 仅返回一条结果时可以省略索引
Double rev2 = [SELECT AnnualRevenue FROM Account WHERE Name = 'Acme' LIMIT 1].AnnualRevenue;

通过关系访问 sObject 字段

关系访问

sObject 记录通过两个字段表示与其他记录的关系:一个是 ID 字段,一个是指向关联 sObject 表示地址的指针字段。例如,Contact 有 AccountId(类型 ID)和 Account(类型 Account,指向关联对象)。

Account a = new Account(Name = 'Acme');
insert a;
Contact c = new Contact(LastName = 'Weissman');
c.AccountId = a.Id;  // 通过 ID 关联
insert c;

// SOQL 查询后 Account 字段被填充
c = [SELECT Account.Name FROM Contact WHERE Id = :c.Id];
// 通过关系遍历修改字段
c.Account.Name = 'salesforce.com';
update c.Account;  // 必须单独更新关联记录

读取 vs 修改:读取时,如果 c.Account 为 null,则 c.Account.Name 返回 null(不抛出 NullPointerException)。修改时,如果 c.Account 为 null,抛出 NullPointerException。sObject 字段键可用于通过外部 ID 解析外键:

Account refAcct = new Account(externalId__c = '12345');
Contact c = new Contact(Account = refAcct, LastName = 'Kay');
insert c;  // 通过外部 ID 关联,比先 SOQL 查询再插入更高效

外键与父子关系查询

外键

SOQL 的 SELECT 语句可以包含外键和父子记录连接。外键连接的结果 sObject 可以使用普通字段符号引用:

// 外键连接
System.debug([SELECT Account.Name FROM Contact WHERE FirstName = 'Caroline'].Account.Name);

// 父子关系——子查询
for (Account a : [SELECT Id, Name, (SELECT LastName FROM Contacts)
    FROM Account WHERE Name = 'Acme']) {
    Contact[] cons = a.Contacts;
}

SOQL 聚合函数

聚合函数

SOQL 中支持聚合函数如 SUM()、MAX()、AVG()、COUNT()。任何包含聚合函数的查询结果返回为AggregateResult 对象数组(只读 sObject,仅用于查询结果)。

// 不带 GROUP BY
AggregateResult[] groupedResults = [SELECT AVG(Amount) aver FROM Opportunity WITH USER_MODE];
Object avgAmount = groupedResults[0].get('aver');

// 带 GROUP BY——更强大的报表功能
AggregateResult[] groupedResults = [SELECT CampaignId, AVG(Amount)
    FROM Opportunity WITH USER_MODE GROUP BY CampaignId];
for (AggregateResult ar : groupedResults) {
    System.debug('Campaign ID: ' + ar.get('CampaignId'));
    System.debug('Average amount: ' + ar.get('expr0'));
}

无别名的聚合字段自动获得隐式别名 expri(i 从 0 开始递增)。聚合函数查询仍受总查询行数限制——除 COUNT() 外,每个聚合行都计入查询行数。GROUP BY 中每个分组计为一条查询行。

处理非常大的 SOQL 查询

大查询

SOQL 查询返回过多 sObject 时可能超出堆大小限制。解决方案是使用 SOQL for 循环(通过内部的 query/queryMore 调用分批处理记录)。

// 可能导致堆溢出
Account[] accts = [SELECT Id FROM Account];

// 推荐:SOQL for 循环(单条格式——循环内不执行 DML 时使用)
for (Account a : [SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%']) {
    // 每条记录处理(不使用 DML)
}

// 推荐:列表格式——循环内执行 DML 时使用(批量处理)
for (List<Account> accts : [SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%']) {
    for (Account a : accts) { /* 处理 */ }
    update accts;  // 批量 DML
}

注意:SOQL for 循环减少了触及堆大小限制的可能性,但可能消耗更多 CPU 周期。对于批量更新,首选方法仍是 Batch Apex(最小化 Governor Limit 风险)。

查询选择性与自定义索引

查询选择性

为获得最佳性能,SOQL 查询必须具有选择性——尤其在触发器内部。当查询过滤器位于索引字段上且将结果行数减少到系统定义的阈值以下时,查询被认为是选择性的。最佳实践:索引字段上的过滤器匹配少于 10% 的总行数。

默认索引字段:主键(Id、Name、OwnerId)、外键(查找/主-详细信息)、审计日期(CreatedDate、SystemModstamp)、RecordType 字段、标记为 External ID 或 Unique 的自定义字段。Salesforce 优化器会自动为高频查询的字段添加索引。多选选择列表、多币种组织中的货币字段、长文本字段和二进制字段上不能创建自定义索引。

索引通常不被使用的情况:查询值超过阈值、使用负运算符(!=、NOT CONTAINS、NOT STARTS WITH)、CONTAINS 运算符扫描行数超过 333,333、与空值比较。

返回单条记录的 SOQL 查询

单条记录

当结果列表只包含一个元素时,SOQL 查询可直接赋值给单个 sObject。零条或多于一条记录时抛出运行时异常。此语法支持 Database.query 方法、安全导航运算符(?.)、空值合并运算符(??)和 Map.values(不推荐)。

// 直接赋值——仅当返回一条记录时有效
Account acct = [SELECT Id FROM Account];
String name = [SELECT Name FROM Account].Name;

性能优化:过滤 Null 值

过滤Null

在 SOQL 和 SOSL 查询的 WHERE 子句中明确过滤掉 null 值,可让 Salesforce 提高查询性能。任何 Thread__c 值为 null 的记录都会被从搜索中消除:

// Thread__c 不为 null 的过滤优化查询性能
for (CSO_CaseThread_Tag__c t : [SELECT Name FROM CSO_CaseThread_Tag__c
    WHERE Thread__c = :threadId AND Thread__c != null]) {
    tagSet.add(t.Name);
}

SOQL 中的多态关系

多态关系

多态关系是对象之间的关系,其中一个引用对象可以是几种不同类型之一。例如,Task 的 Who 关系字段可以是 Contact 或 Lead。

// 方法1:使用 Type 限定符过滤
List<Event> events = [SELECT Description FROM Event
    WHERE What.Type IN ('Account', 'Opportunity')];

// 方法2:使用 TYPEOF 子句
List<Event> events = [SELECT TYPEOF What
    WHEN Account THEN Phone WHEN Opportunity THEN Amount END FROM Event];

// 方法3:instanceof 判断
Event myEvent = eventFromQuery;
if (myEvent.What instanceof Account) {
    // 处理 Account
} else if (myEvent.What instanceof Opportunity) {
    // 处理 Opportunity
}

// 使用前必须将引用对象赋给适当类型的变量
if (merch.Owner instanceof User) {
    User userOwner = merch.Owner;
    processUser(userOwner);
}

Apex 绑定变量

绑定变量

SOQL 和 SOSL 语句可以引用前面带有冒号(:)的 Apex 代码变量和表达式。这种用法称为绑定(bind)。Apex 解析器在 SQL/SOSL 语句执行之前先在代码上下文中评估局部变量。绑定表达式可用于:FIND 子句中的搜索字符串、WHERE 子句中的过滤器字面量、IN/NOT IN 运算符的值、WITH DIVISION 子句、LIMIT 和 OFFSET 子句的数值。

Account A = new Account(Name='xxx'); insert A;
// 简单绑定
Account B = [SELECT Id FROM Account WHERE Id = :A.Id];
// 表达式绑定
B = [SELECT Id FROM Account WHERE Name = :('x' + 'xx')];
B = [SELECT Id FROM Account WHERE Name = :'XXXX'.substring(0,3)];
// IN 绑定——ID 列表
Contact[] cc = [SELECT Id FROM Contact LIMIT 2];
Task[] tt = [SELECT Id FROM Task WHERE WhoId IN :cc];
// String 列表绑定
String[] ss = new String[]{'a', 'b'};
Account[] aa = [SELECT Id FROM Account WHERE AccountNumber IN :ss];
// LIMIT/OFFSET 绑定
Integer i = 1;
B = [SELECT Id FROM Account LIMIT :i];
Integer offsetVal = 10;
List<Account> offsetList = [SELECT Id FROM Account OFFSET :offsetVal];
Apex 绑定变量不支持 DISTANCE 函数中的 units 参数。

查询所有记录:ALL ROWS

ALL ROWS

SOQL 语句可以使用 ALL ROWS 关键字查询组织中的所有记录,包括已删除的记录和已归档的活动。可用于查询回收站中的记录。不能与 FOR UPDATE 关键字一起使用。

System.assertEquals(2, [SELECT COUNT() FROM Contact WHERE AccountId = a.Id ALL ROWS]);

SOQL For 循环

SOQL For

SOQL for 循环遍历 SOQL 查询返回的所有 sObject 记录。两种格式:单条格式(每条记录执行一次,适合无 DML 的场景,简单但不适合批量操作)和列表格式(每批 200 条记录执行一次,在循环内执行 DML 的最佳选择)。

// 单条格式
for (Account a : [SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%']) {
    // 每条记录执行一次,不适合在此使用 DML
}

// 列表格式——批量 200 条,适合 DML
for (List<Account> accts : [SELECT Id, Name FROM Account WHERE Name = 'Siebel']) {
    for (Account a : accts) { a.Name = 'Oracle'; }
    update accts;  // 每次更新最多 200 条
}

// 绑定变量在 for 循环中使用
String s = 'Acme';
for (Account a : [SELECT Id, Name FROM Account WHERE Name LIKE :(s+'%')]) {
    // 处理
}

与标准 SOQL 查询的差异:SOQL for 循环使用 SOAP API 的 query/queryMore 方法进行高效分块,帮助避免堆大小限制。聚合函数不支持 queryMore——如果聚合函数结果超过 2,000 行会抛出运行时异常。

SOQL For 循环:格式与性能

For循环格式

两种格式的执行差异:

// 三条同名 Account
insert new Account[]{new Account(Name='yyy'), new Account(Name='yyy'), new Account(Name='yyy')};

// 单条格式:循环执行 3 次(每条记录一次)
Integer i = 0;
for (Account tmp : [SELECT Id FROM Account WHERE Name = 'yyy']) { i++; }
System.assert(i == 3);

// 列表格式:循环执行 1 次(一批 200 条包含所有 3 条)
Integer i = 0; Integer j;
for (Account[] tmp : [SELECT Id FROM Account WHERE Name = 'yyy']) {
    j = tmp.size(); i++;
}
System.assert(j == 3);   // 列表包含 3 条记录
System.assert(i == 1);   // 循环只执行一次

重要注意事项:break 和 continue 在两种格式中都可以使用(列表格式中 continue 跳到下一批)。DML 每次最多处理 10,000 条,列表 for 循环每批 200 条——如果在循环中对每条记录执行多次 DML 操作,可能触及运行时限制。获取超过 200 条子记录时会抛出 QueryException: Aggregate query has too many rows for direct assignment, use FOR loop

本章全面涵盖了 Apex 中的 SOQL 和 SOSL 查询——从基本的查询语句到高级的聚合函数、关系遍历、多态查询、绑定变量以及 SOQL for 循环的性能优化。