Salesforce Full Sandbox 5000万+记录清理:Apex与SOQL性能优化及限制规避深度实践
理解核心挑战:Governor Limits与大数据量
核心策略一:精通Batch Apex,为大数据而生
1. Database.QueryLocator vs. Iterable
2. 智能调整Batch Size
3. 设计可中断与可恢复的Batch Apex (Database.Stateful)
核心策略二:SOQL查询优化,减少数据库交互
1. 编写选择性查询 (Selective Queries)
2. 避免在execute方法中进行SOQL查询
3. 只查询必要字段
核心策略三:高效的DML操作
1. 坚持批量DML
2. 最小化DML操作
3. 处理部分成功 (allOrNone=false)
核心策略四:利用平台缓存减少重复计算和查询 (Platform Cache)
核心策略五:异步Apex链式调用与Queueable Apex
监控、调试与性能分析
Full Sandbox特定考量
总结与建议
在Salesforce Full Sandbox环境中处理海量数据,特别是涉及数千万甚至上亿条记录的复杂数据清理任务,是对开发者和架构师技能的严峻考验。Full Sandbox因其与生产环境数据量级相似,成为验证大规模数据处理逻辑的最佳场所,但也意味着我们将直面生产环境可能遇到的所有性能瓶颈和Governor Limits。本文将深入探讨如何在Salesforce平台上,利用Apex和SOQL有效处理超过5000万条记录的数据清理任务,重点关注性能优化、Governor Limits规避,特别是CPU时间、堆大小限制,并提供可中断、可恢复的Batch Apex设计模式以及平台缓存的应用策略。
理解核心挑战:Governor Limits与大数据量
Salesforce作为一个多租户平台,通过Governor Limits确保资源公平分配,防止任何单一事务或代码执行消耗过多共享资源。在处理5000万+记录时,以下限制尤为关键:
- Total number of SOQL queries issued: 单个事务中SOQL查询次数限制(同步100,异步200)。
- Total number of records retrieved by SOQL queries: 单个事务中SOQL查询返回的总记录数限制(50,000条)。
- Total number of DML statements issued: 单个事务中DML操作次数限制(150次)。
- Total number of records processed as a result of DML statements: 单个事务中DML操作处理的总记录数限制(10,000条)。
- Maximum CPU time on the Salesforce servers: 单个事务CPU执行时间限制(同步10,000ms,异步60,000ms)。
- Maximum heap size: 单个事务内存(堆)大小限制(同步6MB,异步12MB)。
对于5000万级别的数据,任何单次事务处理都无法覆盖,必须采用异步处理机制,而Batch Apex是首选。但即使是Batch Apex,其execute
方法的每次执行也受限于上述事务限制,特别是CPU时间和堆大小。
核心策略一:精通Batch Apex,为大数据而生
Batch Apex设计用于处理大量记录,它将作业分解为多个块(chunks),每个块在一个独立的事务中执行。这是处理大规模数据的基础。
1. Database.QueryLocator
vs. Iterable<SObject>
对于超过50,000条记录的场景,start
方法必须返回Database.QueryLocator
对象,而不是Iterable<SObject>
。
Iterable<SObject>
: 会尝试将所有记录(或其ID)加载到内存中,对于5000万条记录,这会立即超出12MB的堆限制。Database.QueryLocator
: 它不一次性加载所有记录,而是使用游标逐批检索数据。这使得Batch Apex可以处理高达5000万条记录(这是Database.QueryLocator
的硬性限制)。
// 正确示例:使用QueryLocator处理海量数据
global class MassiveDataCleansingBatch implements Database.Batchable<sObject>, Database.Stateful {
global String query = 'SELECT Id, Field_To_Clean__c, Related_Field__c FROM My_Large_Object__c WHERE Needs_Cleansing__c = true'; // 示例查询
global Database.QueryLocator start(Database.BatchableContext BC) {
// 返回QueryLocator,Salesforce会自动处理记录的分块检索
return Database.getQueryLocator(query);
}
// ... execute 和 finish 方法 ...
}
2. 智能调整Batch Size
Database.executeBatch
方法接受一个可选的scope
参数,用于定义每个execute
方法处理的记录数(默认为200,最大2000)。这个参数至关重要:
- 较小的Scope (e.g., 100-500):
- 优点:减少单次
execute
事务的CPU时间和堆消耗,降低触碰限制的风险。 - 缺点:增加Batch作业的总执行次数,可能延长整体处理时间,并增加SOQL查询(如果
execute
中有查询)和DML操作的总次数(跨多个事务)。
- 优点:减少单次
- 较大的Scope (e.g., 1000-2000):
- 优点:减少总的事务开销,可能缩短整体时间。
- 缺点:增加单次
execute
事务的资源消耗,更容易达到CPU时间或堆大小限制,特别是当处理逻辑复杂时。
建议: 从较小的Scope(如200或500)开始测试。监控Apex Jobs页面中的作业状态和错误信息,特别是CPU超时。根据实际运行情况和错误日志逐步调整Scope大小,找到性能和稳定性之间的最佳平衡点。对于5000万条记录,即使Scope为2000,也需要执行25000次execute
事务,总时长会很可观。
3. 设计可中断与可恢复的Batch Apex (Database.Stateful
)
处理如此庞大的数据集,作业几乎不可能一次性成功运行完毕。网络波动、平台维护、代码逻辑错误或其他未预见的Governor Limit问题都可能导致作业中断。因此,设计可恢复性至关重要。
使用
Database.Stateful
: 在类定义中实现Database.Stateful
接口。这允许在execute
方法之间保持成员变量的状态。记录处理进度: 在
Database.Stateful
的成员变量中跟踪关键进度信息。例如:- 已成功处理的记录数。
- 遇到的错误数。
- 最后一个成功处理的记录ID(如果处理是顺序的)。
- 或者,更复杂的检查点信息。
global class ResumableMassiveDataCleansingBatch implements Database.Batchable<sObject>, Database.Stateful {
global String query = 'SELECT Id, Field_To_Clean__c, Status__c FROM My_Large_Object__c WHERE Status__c = \'Pending\' ORDER BY Id'; // 按ID排序是可恢复设计的关键之一
global Id lastProcessedId = null; // 存储上次成功处理的最后一个ID
global Integer recordsProcessed = 0;
global Integer errorsEncountered = 0;
// 构造函数,允许传入上次的断点ID
global ResumableMassiveDataCleansingBatch(Id startingId) {
if (startingId != null) {
this.query = 'SELECT Id, Field_To_Clean__c, Status__c FROM My_Large_Object__c WHERE Status__c = \'Pending\' AND Id > :startingId ORDER BY Id';
}
}
// 默认构造函数
global ResumableMassiveDataCleansingBatch() {
// 使用默认查询
}
global Database.QueryLocator start(Database.BatchableContext BC) {
// 如果提供了startingId,查询会从该ID之后开始
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext BC, List<My_Large_Object__c> scope) {
List<My_Large_Object__c> recordsToUpdate = new List<My_Large_Object__c>();
Id currentLastId = null;
for (My_Large_Object__c record : scope) {
try {
// ** 执行复杂的清理逻辑 **
// 假设清理逻辑封装在一个Helper类中
DataCleansingHelper.cleanRecord(record);
record.Status__c = 'Processed';
recordsToUpdate.add(record);
currentLastId = record.Id; // 跟踪当前批次最后一个处理的ID
} catch (Exception e) {
// ** 强大的错误处理机制 **
// Log the error (e.g., to a custom object, Platform Event)
System.debug('Error processing record ' + record.Id + ': ' + e.getMessage());
errorsEncountered++;
// 可以考虑将错误记录的状态标记为 'Error'
// record.Status__c = 'Error';
// recordsToUpdate.add(record); // Optionally update errored records
}
}
if (!recordsToUpdate.isEmpty()) {
// 使用部分成功模式,避免一个坏记录影响整个Chunk
Database.SaveResult[] saveResults = Database.update(recordsToUpdate, false);
// 检查并记录DML错误
Integer successCount = 0;
for (Integer i = 0; i < saveResults.size(); i++) {
if (saveResults[i].isSuccess()) {
successCount++;
} else {
errorsEncountered++;
// 记录详细的DML错误
String errMsg = 'DML Error on record ID ' + recordsToUpdate[i].Id + ': ';
for(Database.Error err : saveResults[i].getErrors()) {
errMsg += err.getStatusCode() + ': ' + err.getMessage() + ' Fields: ' + err.getFields() + '. ';
}
System.debug(errMsg);
// 可以在这里记录到自定义日志对象
}
}
recordsProcessed += successCount;
// 只有在至少有一条记录成功更新后,才更新lastProcessedId
// 或者根据业务需求,即使有错误也继续前进,只记录最后一个尝试的ID
if(successCount > 0 && currentLastId != null) {
lastProcessedId = currentLastId;
}
}
}
global void finish(Database.BatchableContext BC) {
System.debug('Batch Job Finished.');
System.debug('Total Records Processed Successfully: ' + recordsProcessed);
System.debug('Total Errors Encountered: ' + errorsEncountered);
System.debug('Last Processed Record ID (if applicable): ' + lastProcessedId);
// ** 自动重启逻辑 (可选) **
// 如果作业因超时等原因中断,可以设计一个机制来检查状态并重新启动
// 例如,finish方法可以检查是否还有 'Pending' 状态的记录
// 如果有,并且没有达到最大重试次数,则可以再次调用 Database.executeBatch
// 注意:需要更复杂的逻辑来管理重试次数和防止无限循环
// 可以通过 Custom Metadata 或 Custom Setting 控制是否自动重启及重启参数
// ** 发送通知 **
// Email, Chatter Post, Custom Notification
// Example: Send completion email
// Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
// ... setup email ...
// Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
如何实现重启?
- 手动重启: 作业失败后,管理员检查
finish
方法的日志(或自定义日志对象)获取lastProcessedId
,然后手动执行Database.executeBatch(new ResumableMassiveDataCleansingBatch(lastProcessedId), scopeSize);
。 - 自动链式重启: 在
finish
方法中,查询是否还存在需要处理的记录(Status__c = 'Pending'
)。如果存在,并且当前作业不是因为严重错误(如代码异常)而是可能因为超时等原因停止,可以再次调用Database.executeBatch
启动一个新的实例,并将lastProcessedId
传入。这种方式需要非常小心地设计退出条件,防止无限循环。通常会结合自定义设置来控制最大重试次数或总执行时间。 - 调度器监控重启: 一个独立的Scheduled Apex类定期检查Batch Job的状态和自定义日志。如果发现某个长时间运行的清理作业失败了,它可以根据日志中的
lastProcessedId
重新调度该Batch。
关键点:
start
方法的查询必须包含ORDER BY Id
,这样才能基于lastProcessedId
可靠地恢复。execute
方法需要健壮的错误处理,避免单个记录的问题中断整个chunk或job。Database.Stateful
会增加 Batch Apex 的序列化和反序列化开销,如果状态变量变得非常大(虽然不太可能只存ID和计数器),也可能影响性能。但对于可恢复性来说,这是必要的代价。
核心策略二:SOQL查询优化,减少数据库交互
即使在Batch Apex中,低效的SOQL也会累积成巨大的性能问题。
1. 编写选择性查询 (Selective Queries)
- 核心原则:
WHERE
子句应尽可能过滤掉大部分记录,理想情况下使用索引字段。 - 索引字段:
- 标准索引字段:
Id
,Name
,CreatedDate
,LastModifiedDate
,RecordTypeId
,OwnerId
,Master-Detail Relationship Fields
,Lookup Relationship Fields
(需要管理员请求Salesforce创建索引)。 - 自定义索引字段:
External ID
字段、Unique
字段会自动创建索引。管理员可以为其他自定义字段(文本、数字、日期、复选框等)请求Salesforce创建自定义索引。
- 标准索引字段:
- 如何判断选择性: 如果查询条件能将目标记录缩小到总记录数的10%以下(对于标准索引)或5%以下(对于自定义索引),通常被认为是选择性的。对于大数据量,这个比例甚至需要更低。
- 示例: 假设
My_Large_Object__c
有5000万条记录。WHERE Name = 'SpecificName'
(如果Name是唯一的,选择性极高)WHERE Status__c = 'Pending'
(如果只有100万条是Pending状态,选择性尚可)WHERE Custom_Field__c != null
(如果大部分记录该字段为空,选择性差)WHERE Custom_NonIndexed_Field__c = 'SomeValue'
(选择性差,会导致全表扫描,极慢)
在start
方法的QueryLocator
查询以及execute
方法中可能存在的辅助查询(应尽量避免)中,都要应用此原则。
2. 避免在execute
方法中进行SOQL查询
理想情况下,start
方法获取所有必要的数据。execute
方法内不应再有SOQL查询。如果确实需要关联数据:
- 在
start
查询中通过关系查询(Parent-to-Child / Child-to-Parent)获取。例如:SELECT Id, Name, Account.Name FROM Contact WHERE ...
。 - 如果需要的数据无法通过单一
QueryLocator
获取(例如,需要基于scope
中的记录去查询完全不同的对象),考虑在execute
方法开始时,批量查询一次所需数据,放入Map中供后续处理。但要极其小心,确保这个查询本身是选择性的,并且返回的数据量不会撑爆堆内存。
// execute 方法中需要关联数据的反模式(应避免或优化)
global void execute(Database.BatchableContext BC, List<My_Large_Object__c> scope) {
// **反模式:在循环中查询**
// for (My_Large_Object__c record : scope) {
// Related_Data__c related = [SELECT Id FROM Related_Data__c WHERE Lookup__c = :record.Id LIMIT 1];
// // ... logic ...
// } // 极易触发 SOQL 101 限制!
// **稍好的模式:批量查询一次**
Set<Id> relatedLookupIds = new Set<Id>();
for (My_Large_Object__c record : scope) {
if (record.Lookup_To_Related__c != null) {
relatedLookupIds.add(record.Lookup_To_Related__c);
}
}
if (!relatedLookupIds.isEmpty()) {
// 确保这个查询高效且返回数据量可控
Map<Id, Related_Data__c> relatedDataMap = new Map<Id, Related_Data__c>(
[SELECT Id, Relevant_Field__c FROM Related_Data__c WHERE Id IN :relatedLookupIds]
);
for (My_Large_Object__c record : scope) {
Related_Data__c related = relatedDataMap.get(record.Lookup_To_Related__c);
if (related != null) {
// ... 使用 related.Relevant_Field__c ...
}
}
}
// ... DML 操作 ...
}
3. 只查询必要字段
SELECT Id, Field1__c, Field2__c ...
而不是 SELECT FIELDS(ALL)
或 SELECT FIELDS(STANDARD)
。查询的字段越少:
- 查询执行速度越快。
- 网络传输数据量越少。
- 占用的堆内存越少。
对于5000万记录,即使每个记录节省几个字节,累积起来的内存和性能差异也会非常显著。
核心策略三:高效的DML操作
1. 坚持批量DML
这是Apex开发的基础,但在大数据场景下尤其重要。永远不要在循环中执行DML操作。将需要更新/删除的记录收集到List中,在循环外执行一次DML。
List<My_Large_Object__c> recordsToUpdate = new List<My_Large_Object__c>();
for (My_Large_Object__c record : scope) {
// ... 清理逻辑 ...
if (recordIsDirty) { // 只有真正需要更新的才加入列表
recordsToUpdate.add(record);
}
}
if (!recordsToUpdate.isEmpty()) {
Database.update(recordsToUpdate, false); // 使用部分成功模式
}
2. 最小化DML操作
- 条件更新: 在将记录添加到DML列表之前,检查是否真的需要更新。如果清理逻辑没有改变记录的任何字段值,就不应该执行
update
。 - 合并操作: 能否通过一次
update
完成多个字段的清理?避免对同一记录执行多次DML。
3. 处理部分成功 (allOrNone=false
)
在Database.update()
, Database.insert()
, Database.delete()
等方法中,将第二个参数allOrNone
设为false
。这样,即使List中的部分记录因为校验规则、触发器错误等原因失败,其他成功的记录仍然会被提交。这对于长时间运行的批量作业至关重要,可以避免因为少数坏数据导致整个chunk失败回滚。
务必检查返回的Database.SaveResult[]
或Database.DeleteResult[]
数组,记录下失败的记录及其原因,以便后续处理。
核心策略四:利用平台缓存减少重复计算和查询 (Platform Cache)
当某些数据在Batch的不同execute
事务之间需要被反复查询或计算时,平台缓存(Platform Cache)可以显著提升性能并减少SOQL消耗。
适用场景:
- 配置数据: 存储在Custom Metadata或Custom Settings中的配置信息,这些信息在整个Batch Job执行期间通常不变。
- 少量关键关联数据: 例如,一个大型客户下的所有联系人的某些聚合信息,如果这个客户的记录在多个chunk中都可能被处理到。
- 复杂计算结果: 如果某个计算非常耗时(CPU密集),并且其输入在多个chunk间可能重复,可以将结果缓存起来。
类型:
- Org Cache: 组织范围共享,跨用户、跨会话、跨事务。这是Batch Apex场景下最常用的缓存类型。需要预先在“设置”中分配缓存空间。
- Session Cache: 用户会话级别,不适用于Batch Apex。
使用示例:
// 假设有一个配置Custom Metadata Type: Cleansing_Rules__mdt
// 我们不希望在每个execute方法中都查询它
global class MassiveDataCleansingBatch implements Database.Batchable<sObject>, Database.Stateful {
// ... 其他变量 ...
private static final String CACHE_KEY_RULES = 'CleansingRules';
private Map<String, Cleansing_Rule__mdt> rulesMap;
global Database.QueryLocator start(Database.BatchableContext BC) {
// ** 在start方法中预热缓存 **
ensureRulesInCache();
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext BC, List<My_Large_Object__c> scope) {
// ** 从缓存中获取规则 **
if (rulesMap == null) { // 如果Stateful状态丢失或首次执行
getRulesFromCache();
}
List<My_Large_Object__c> recordsToUpdate = new List<My_Large_Object__c>();
for (My_Large_Object__c record : scope) {
// 使用缓存的rulesMap进行清理逻辑判断
if (rulesMap != null && rulesMap.containsKey(record.RecordTypeId)) {
Cleansing_Rule__mdt rule = rulesMap.get(record.RecordTypeId);
// ... 应用规则 ...
}
// ... 其他逻辑 ...
}
// ... DML ...
}
global void finish(Database.BatchableContext BC) {
// 可选:清理缓存,虽然Org Cache有TTL,但如果确定不再需要可以手动移除
// Cache.Org.remove(CACHE_KEY_RULES);
}
// 辅助方法:确保规则在缓存中
private void ensureRulesInCache() {
if (!Cache.Org.contains(CACHE_KEY_RULES)) {
List<Cleansing_Rule__mdt> rules = [SELECT DeveloperName, Field_API_Name__c, Cleansing_Logic__c FROM Cleansing_Rule__mdt];
Map<String, Cleansing_Rule__mdt> tempRulesMap = new Map<String, Cleansing_Rule__mdt>();
for(Cleansing_Rule__mdt rule : rules) {
tempRulesMap.put(rule.DeveloperName, rule); // 或者按其他需要的方式组织
}
// 存入Org Cache,设置合适的TTL(Time-To-Live),例如1小时 (3600秒)
Cache.Org.put(CACHE_KEY_RULES, tempRulesMap, 3600);
this.rulesMap = tempRulesMap; // 同时赋值给成员变量
} else {
getRulesFromCache(); // 如果缓存已存在,加载到成员变量
}
}
// 辅助方法:从缓存加载规则
private void getRulesFromCache(){
if (Cache.Org.contains(CACHE_KEY_RULES)){
this.rulesMap = (Map<String, Cleansing_Rule__mdt>)Cache.Org.get(CACHE_KEY_RULES);
}
}
}
- 注意事项:
- 缓存命中率: 只有当数据被重复访问时,缓存才有意义。
- 缓存失效: Org Cache有TTL。如果源数据(如Custom Metadata)在Batch执行期间被修改,缓存中的数据不会自动更新,可能导致处理逻辑错误。需要设计合适的TTL或在必要时手动清除缓存。
- 缓存大小限制: Org Cache有容量限制,不要试图缓存过大的数据集。
- 序列化成本: 存入缓存的对象必须是可序列化的,存取本身也有开销,对于非常小的、查询极快的数据,缓存可能得不偿失。
核心策略五:异步Apex链式调用与Queueable Apex
对于极其复杂、多阶段的清理任务,单一Batch Apex可能不够灵活或难以管理。可以考虑:
- 链式Batch Apex: 在第一个Batch Job的
finish
方法中,判断是否需要启动下一个阶段的Batch Job,并调用Database.executeBatch
启动它。例如,Batch 1 识别并标记记录,Batch 2 对标记的记录执行清理,Batch 3 生成报告。 - Batch Apex 调用 Queueable Apex:
finish
方法可以启动一个或多个Queueable Apex作业来执行后续的、不适合用Batch处理的任务(例如,调用外部系统、执行一些需要不同限制或上下文的操作)。Queueable Apex相比Future方法提供了更好的灵活性(支持非基本类型参数、可以链式调用、有Job ID)。
架构模式考量:
Staging Object Pattern: 如果清理逻辑非常复杂,且直接操作原对象可能引发过多自动化(触发器、流)冲突或性能问题,可以考虑:
- Batch 1: 将需要清理的记录(或其子集)复制到专门的Staging Object。
- Batch 2 (或多个Batch): 在Staging Object上执行复杂的清理和转换逻辑。这里的自动化可以被精简或禁用。
- Batch 3: 将清理后的结果从Staging Object写回原对象,或者直接删除原对象中对应的数据。
- 优点: 隔离复杂逻辑,减少对主对象自动化的影响,更易于测试和管理。
- 缺点: 增加了数据存储(Staging Object),需要额外的ETL步骤,整体流程更长。
Flag-Based Processing: 在主对象上增加一个状态字段(如
Cleansing_Status__c
)或复选框(Needs_Cleansing__c
)。- Batch 1 (或外部数据加载): 识别记录,设置
Needs_Cleansing__c = true
。 - Batch 2 (主清理Batch):
start
查询WHERE Needs_Cleansing__c = true
。在execute
中处理记录,处理完成后设置Needs_Cleansing__c = false
或更新Cleansing_Status__c
。
- 优点: 逻辑清晰,易于跟踪进度,易于重跑失败的记录。
- 缺点: 需要修改主对象结构,对记录的额外更新操作。
- Batch 1 (或外部数据加载): 识别记录,设置
监控、调试与性能分析
处理大数据量时,有效的监控和调试至关重要。
- Apex Jobs: 监控Batch作业的状态、已处理批次数、失败次数。
- Debug Logs: 对于Batch Apex,Debug Log非常有限。通常只能捕获
start
、finish
以及少量execute
方法的日志(除非开启特定用户的跟踪标志,但这在Full Sandbox中处理大量数据时可能不现实或性能影响过大)。日志大小也有限制。 - 自定义日志框架: 这是最可靠的方式。创建一个自定义对象(如
Batch_Job_Log__c
),包含字段:Job_ID__c
,Apex_Class_Name__c
,Timestamp__c
,Log_Level__c
(INFO, ERROR, DEBUG),Message__c
,Related_Record_ID__c
(可选)。- 在
execute
方法的catch
块中记录错误详情。 - 在
finish
方法中记录总结信息。 - 注意: DML操作记录日志本身也受Governor Limits约束。可以考虑:
- 批量插入日志记录。
- 使用Platform Events发布日志事件,由另一个轻量级触发器或进程异步写入日志对象。这可以解耦日志记录与主处理逻辑,减少对主事务的影响。
- 在
- 性能分析:
- 利用
Limits
类 (Limits.getCpuTime()
,Limits.getHeapSize()
,Limits.getQueries()
等) 在execute
方法的关键点打印资源消耗,帮助定位瓶颈。 - 分析
start
查询的查询计划 (Query Plan Tool in Developer Console),确保使用了索引。 - 在Full Sandbox中进行多次测试运行,记录不同Batch Size下的总耗时、CPU超时频率、堆超限频率。
- 利用
Full Sandbox特定考量
- 数据时效性: Full Sandbox的刷新周期较长。确保测试期间的数据状态与预期场景一致。
- 资源竞争: Full Sandbox是共享资源。如果组织内有其他大规模数据操作或集成在同时运行,可能会相互影响性能,导致测试结果不稳定。
- 测试覆盖: 确保测试覆盖各种数据场景和边界条件,特别是可能导致错误的脏数据。
总结与建议
在Salesforce Full Sandbox中优化处理超过5000万条记录的数据清理任务,是一项涉及架构设计、代码实现和性能调优的综合性挑战。核心策略包括:
- 拥抱Batch Apex: 使用
Database.QueryLocator
处理大数据集。 - 精细调整Batch Size: 在性能和稳定性间找到平衡。
- 设计可恢复性: 实现
Database.Stateful
并记录处理断点,结合ORDER BY Id
和错误处理。 - 优化SOQL: 编写选择性查询,避免
execute
中的查询,只查询必要字段。 - 高效DML: 坚持批量操作,使用部分成功模式,最小化DML次数。
- 善用Platform Cache: 缓存不变的配置或少量重复访问的数据。
- 考虑高级模式: 链式Batch、Queueable Apex、Staging Object或Flag-Based处理。
- 强化监控: 依赖自定义日志框架而非Debug Logs。
没有万能的解决方案。你需要根据具体的业务需求、数据结构、清理逻辑的复杂度以及实际的性能测试结果,不断迭代和优化你的Apex代码和架构设计。在Full Sandbox中彻底测试是确保方案在生产环境成功的关键。记住,耐心、细致和对Governor Limits的深刻理解是克服这一挑战的基石。