🧠 Avoiding forUpdate and Full Table Scans in D365 FO: Lessons from Tier-5 Performance Testing
When stress-testing a Tier-5 Premier Performance environment in Dynamics 365 Finance & Operations (D365 FO), performance bottlenecks become painfully visible.
Recently, while running a work completion load script with 25 concurrent workers, we repeatedly hit 500 Internal Server Errors. After reviewing logs and SQL traces, the primary culprit was clear:
❌ forUpdate inside loops and full table scans on large transactional tables caused SQL blocking, deadlocks, and lock escalation under heavy load.
🔍 Why forUpdate Inside Loops Can Hurt Performance
When you do this:
while select forUpdate rec from WMSOrderTrans
{
rec.WorkCompleted = true;
rec.update();
}
Each iteration triggers:
In performance test environments, this can collapse under concurrency, resulting in timeouts & 500 errors.
✅ Initial Optimization Idea: Bulk Fetch, Then Update
A common optimization is:
List recIds = new List(Types::Int64);
// Step 1: Fetch all RecIds (no locks)
while select RecId from WMSOrderTrans where Status == Open
{
recIds.addEnd(WMSOrderTrans.RecId);
}
// Step 2: Update in one transaction
ttsBegin;
for (int i = 1; i <= recIds.length(); i++)
{
WMSOrderTrans rec = WMSOrderTrans::findRecId(recIds.get(i), true);
rec.WorkCompleted = true;
rec.update();
}
ttsCommit;
This reduces SQL round-trips, improves throughput, and avoids long-running locks.
⚠️ BUT… What About Concurrency?
A valid concern arises in multi-user environments:
Between Step 1 and Step 2, the record’s status could have changed, another process might have updated it, or it might now be locked.
In transactional scenarios (e.g., live systems), this introduces a stale update risk.
That’s why some recommend keeping ttsBegin and ttsCommit inside the while select, so each record is fetched, locked, and updated immediately ensuring data consistency, but at the cost of lower performance under bulk load.
✅ The Balanced Middle Ground
For scenarios requiring both performance & safety, add status re-validation before updating:
Recommended by LinkedIn
List recIds = new List(Types::Int64);
// Step 1: Bulk fetch candidate records
while select RecId from WMSOrderTrans where Status == Open
{
recIds.addEnd(WMSOrderTrans.RecId);
}
// Step 2: Update only if still valid
ttsBegin;
for (int i = 1; i <= recIds.length(); i++)
{
WMSOrderTrans rec = WMSOrderTrans::findRecId(recIds.get(i), true);
// ✅ Double-check status to avoid stale updates
if (rec.Status == Open)
{
rec.WorkCompleted = true;
rec.update();
}
}
ttsCommit;
✔ Better performance – fewer SQL calls
✔ Still safe – prevents overwriting newer changes
✔ Concurrency-friendly – avoids stale updates
📊 When to Use Which Approach
Batch jobs or Tier-5 Performance Tests (isolated environments)
Real-time, multi-user transactional updates
Mixed or unpredictable environments
🔄 Visualizing the Difference
Here’s a quick flowchart to compare Safe Inline Updates vs Bulk Update with Re-validation:
💡 Key Takeaways
Performance isn’t just about hardware—it’s about writing concurrency-aware, scalable code.
Have you faced a similar challenge in your D365 FO projects? How do you balance performance vs consistency? Let’s discuss!
#D365FO #Dynamics365 #PerformanceTesting #Concurrency #Tier5 #SQLTuning #Xpp #MicrosoftDynamics
This approach is wrong when you jump from step 1 to step 2 in that fraction of seconds possibility of original record status would be changed. Best way is don’t write for ttsbegin and ttscomit outside of the while self, it should be with inside the while select for better performance.
Feba Thomas Naveen Ajmal Mohammed Shashidharan P