Let me provide a comprehensive optimization solution covering all the key performance areas:
Groovy Script Optimization:
Your current script has the classic N+1 query anti-pattern. Here’s the optimized version using bulk loading:
BEFORE (Inefficient - 1500+ queries for 500 opportunities):
opportunities.each { opp ->
def quote = Quote.findByOpportunity(opp.Id)
def account = Account.findById(opp.AccountId)
def products = Product.findByIds(opp.ProductIds)
// Process each record - 3 queries per opportunity
}
AFTER (Optimized - 3 bulk queries total):
// 1. Extract all IDs first
def oppIds = opportunities.collect { it.Id }
def accountIds = opportunities.collect { it.AccountId }.unique()
def allProductIds = opportunities.collectMany { it.ProductIds ?: [] }.unique()
// 2. Bulk load all related records
def quotesMap = Quote.findAll("OpportunityId IN :oppIds", [oppIds: oppIds])
.collectEntries { [it.OpportunityId, it] }
def accountsMap = Account.findByIds(accountIds)
.collectEntries { [it.Id, it] }
def productsMap = Product.findByIds(allProductIds)
.collectEntries { [it.Id, it] }
// 3. Process with cached lookups (no additional queries)
opportunities.each { opp ->
def quote = quotesMap[opp.Id]
def account = accountsMap[opp.AccountId]
def products = opp.ProductIds.collect { productsMap[it] }
// Process using cached data
}
This reduces 1500+ queries to just 3 bulk queries, improving performance by 100-500x for large batches.
Batch API Usage:
Replace individual save operations with bulk updates:
BEFORE (Inefficient - 500 individual commits):
opportunities.each { opp ->
opp.DiscountApproved = calculateDiscount(opp)
opp.save() // Individual database commit
}
AFTER (Optimized - Single bulk commit):
// Prepare all updates first
def updates = opportunities.collect { opp ->
opp.DiscountApproved = calculateDiscount(opp)
return opp
}
// Bulk update in single transaction
BulkDataAPI.updateRecords('Opportunity', updates, [
batchSize: 200,
allOrNone: false,
bypassTriggers: false
])
The Batch API processes records in optimized chunks and uses a single database transaction, reducing commit overhead from 500 operations to 1.
Database Query Caching:
Implement multi-level caching for reference data:
class WorkflowCache {
// Static cache persists across workflow executions
private static Map<String, Object> staticCache = [:]
// Instance cache for single execution
private Map<String, Object> instanceCache = [:]
def getProduct(productId, useStaticCache = true) {
def cacheKey = "product_${productId}"
// Check instance cache first
if (instanceCache[cacheKey]) {
return instanceCache[cacheKey]
}
// Check static cache for reference data
if (useStaticCache && staticCache[cacheKey]) {
instanceCache[cacheKey] = staticCache[cacheKey]
return staticCache[cacheKey]
}
// Query database only if not cached
def product = Product.findById(productId)
instanceCache[cacheKey] = product
if (useStaticCache) {
staticCache[cacheKey] = product
}
return product
}
def clearInstanceCache() {
instanceCache.clear()
}
static def clearStaticCache() {
staticCache.clear()
}
}
// Usage in workflow
def cache = new WorkflowCache()
opportunities.each { opp ->
def products = opp.ProductIds.collect { cache.getProduct(it) }
// Products are cached across opportunities
}
Performance Profiling:
Implement comprehensive profiling to identify bottlenecks:
class PerformanceProfiler {
private Map<String, Long> timings = [:]
private Map<String, Integer> counts = [:]
def startTimer(String operation) {
timings["${operation}_start"] = System.currentTimeMillis()
}
def endTimer(String operation) {
def start = timings["${operation}_start"]
def duration = System.currentTimeMillis() - start
timings[operation] = (timings[operation] ?: 0) + duration
counts[operation] = (counts[operation] ?: 0) + 1
return duration
}
def logResults() {
logger.info("=== Performance Profile ===")
timings.each { operation, totalTime ->
if (!operation.endsWith('_start')) {
def count = counts[operation]
def avgTime = totalTime / count
logger.info("${operation}: ${totalTime}ms total, ${count} calls, ${avgTime}ms avg")
}
}
}
}
// Usage
def profiler = new PerformanceProfiler()
profiler.startTimer('bulk_load_quotes')
def quotesMap = Quote.findAll("OpportunityId IN :oppIds", [oppIds: oppIds])
.collectEntries { [it.OpportunityId, it] }
profiler.endTimer('bulk_load_quotes')
profiler.startTimer('process_opportunities')
opportunities.each { opp ->
profiler.startTimer('calculate_discount')
def discount = calculateDiscount(opp)
profiler.endTimer('calculate_discount')
profiler.startTimer('update_quote')
updateQuote(quotesMap[opp.Id], discount)
profiler.endTimer('update_quote')
}
profiler.endTimer('process_opportunities')
profiler.logResults()
Complete Optimized Workflow Script:
Here’s the full production-ready implementation:
import oracle.apps.crmCommon.bulkData.BulkDataAPI
class OptimizedOpportunityProcessor {
def cache = new WorkflowCache()
def profiler = new PerformanceProfiler()
def processOpportunities(opportunities) {
profiler.startTimer('total_processing')
try {
// Step 1: Bulk load all related data
profiler.startTimer('bulk_data_loading')
def relatedData = loadRelatedData(opportunities)
profiler.endTimer('bulk_data_loading')
// Step 2: Process opportunities with cached data
profiler.startTimer('opportunity_processing')
def updates = processWithCache(opportunities, relatedData)
profiler.endTimer('opportunity_processing')
// Step 3: Bulk save updates
profiler.startTimer('bulk_save')
saveInBulk(updates)
profiler.endTimer('bulk_save')
profiler.endTimer('total_processing')
profiler.logResults()
return [success: true, processed: opportunities.size()]
} catch (Exception e) {
logger.error("Processing failed: ${e.message}", e)
profiler.logResults() // Log partial results for troubleshooting
throw e
}
}
private def loadRelatedData(opportunities) {
def oppIds = opportunities.collect { it.Id }
def accountIds = opportunities.collect { it.AccountId }.unique()
def productIds = opportunities.collectMany { it.ProductIds ?: [] }.unique()
return [
quotes: Quote.findAll("OpportunityId IN :ids", [ids: oppIds])
.collectEntries { [it.OpportunityId, it] },
accounts: Account.findByIds(accountIds)
.collectEntries { [it.Id, it] },
products: Product.findByIds(productIds)
.collectEntries { [it.Id, it] }
]
}
private def processWithCache(opportunities, relatedData) {
return opportunities.collect { opp ->
def quote = relatedData.quotes[opp.Id]
def account = relatedData.accounts[opp.AccountId]
def products = opp.ProductIds.collect { relatedData.products[it] }
// Business logic
opp.DiscountApproved = calculateDiscount(opp, account, products)
opp.QuoteStatus = determineQuoteStatus(quote, opp.DiscountApproved)
opp.ProcessedDate = new Date()
return opp
}
}
private def saveInBulk(updates) {
// Split into chunks for optimal performance
def chunkSize = 200
updates.collate(chunkSize).each { chunk ->
BulkDataAPI.updateRecords('Opportunity', chunk, [
batchSize: chunkSize,
allOrNone: false
])
}
}
}
// Execute optimized processor
def processor = new OptimizedOpportunityProcessor()
processor.processOpportunities(opportunities)
Performance Benchmarks:
Expected improvements after optimization:
Before optimization (500 opportunities):
- Total time: 6-8 hours
- Database queries: ~1,500
- Database commits: 500
- Memory usage: Low (but inefficient)
After optimization (500 opportunities):
- Total time: 5-8 minutes (60-90x faster)
- Database queries: 3-5 bulk queries
- Database commits: 3 (chunked)
- Memory usage: Moderate (efficient bulk processing)
Implementation Steps:
- Deploy optimized script to test environment
- Run with 50 opportunities - verify results match original
- Gradually increase batch size: 100, 250, 500
- Monitor performance metrics at each level
- Review profiler logs to identify any remaining bottlenecks
- Deploy to production with monitoring enabled
- Schedule nightly batch job with optimized script
Monitoring and Maintenance:
Set up ongoing performance monitoring:
- Log processing time for each batch execution
- Alert if processing time exceeds 15 minutes for 500 records
- Weekly review of profiler logs to identify optimization opportunities
- Monthly cache hit rate analysis
- Quarterly review of batch size optimization
This comprehensive optimization addresses all four focus areas: Groovy script efficiency through bulk loading, Batch API usage for updates, database query caching for reference data, and performance profiling for continuous improvement. Your batch processing time should decrease from 6-8 hours to under 10 minutes for 500 opportunities.
This draft is based on general Oracle CX Cloud knowledge. It has not been verified against your specific version and environment. Practitioners: verify the steps and share your experience below.