Performance problems in Dynamics 365 Finance & Operations arrive as feelings: “posting is slow”, “the warehouse waits for the screen”, “the overnight batch is still running at nine”. This guide describes the order in which we work through them. None of it is magic. The value is in doing the steps in sequence and not skipping to a favourite fix.
1. Turn the complaint into a scenario
Start with something you can repeat. A good performance scenario names:
- The user action or job — “post a sales invoice with 150 lines”, “run inventory closing for one legal entity”, “import the daily price file”.
- How long it takes now and how long is acceptable.
- When it happens — always, at month-end, only for certain customers, only when a specific batch job runs.
- Where it can be reproduced — ideally a sandbox with a recent copy of production data.
Without this, every change becomes a guess and there is no way to prove an improvement.
2. Find out which layer dominates
A single slow operation can spend its time in very different places:
| Symptom | Usual suspects |
|---|---|
| Slow for everyone, all the time | Missing or unsuitable indexes, heavy form data sources, expensive display methods |
| Slow only at certain times | Blocking from batch jobs, competing integrations, data volume peaks |
| Slow for some records only | Data skew, parameter-sensitive query plans, unusual configuration |
| Gets slower month by month | Table growth without cleanup, log and staging tables, history tables |
| Slow only when integrated | Synchronous calls to external systems inside a transaction |
The point of the first measurement is not to fix anything. It is to find out whether time goes into X++ execution, SQL statements, waiting on locks, or calls to something outside the ERP.
3. Trace the execution path
In a sandbox or development environment, a trace captured with the platform’s tracing tools shows the X++ call tree and the SQL statements executed underneath it, with their durations and counts. Two patterns are worth looking for first:
- One expensive statement. A single query with a scan over a large table usually points to an index that does not match the ranges and sort order the application uses, or to a join that cannot use an index at all.
- Many cheap statements. Hundreds or thousands of fast single-row queries add up. This is the classic query-inside-a-loop pattern, and it is fixed in X++, not in the database.
For production, where direct database access is not available, the environment’s monitoring and SQL insight tools show the most expensive and most frequent queries, blocking and wait statistics over time. They are often enough to confirm what the sandbox trace suggests.
4. Fix the largest cost first
Typical fixes, roughly in the order we encounter them:
- Index changes in the application metadata, designed for the actual query pattern rather than added blindly.
- Set-based X++. Replacing row-by-row loops with joins,
insert_recordset,update_recordsetorRecordInsertList— and checking whether overridden table methods force row-by-row behaviour back. - Field lists and caching. Selecting only the fields that are needed, and using the correct table cache settings for genuinely static data.
- Batch design. Splitting large jobs into tasks that can run in parallel, separating jobs that compete for the same tables, and giving long-running processes a dedicated window.
- Moving work out of the transaction. Replacing a synchronous external call inside posting logic with a business event or queue message processed afterwards.
- Data volume. Running the standard cleanup jobs, archiving where supported and stopping custom log tables from growing forever.
Change one thing at a time. When several fixes are applied at once, nobody knows which one mattered — or which one made something else worse.
5. Verify against the baseline
Re-run the scenario from step one in the same environment and compare. If the improvement is real, it should be visible without statistics. Then check the side effects: other scenarios that use the same tables, batch jobs that share the window, and integrations that call the changed code.
6. Keep it from regressing
Most performance problems return because the conditions that created them still exist. After the fix, leave behind:
- Cleanup and archiving jobs scheduled and owned by someone.
- Batch scheduling rules that describe which jobs may run together.
- Coding guidelines for the patterns that caused the problem.
- A short list of scenarios to re-check after each service update or major release.
Performance work is engineering, not tuning by instinct. The tooling in D365 F&O is good enough to find almost any bottleneck — as long as the investigation starts with a clear scenario and ends with a measured result.