InventSum is one of the key tables for D365FO performance. It stores the on-hand inventory: one record for every combination of an item and its inventory dimensions (site, warehouse, location, inventory status, license plate, batch, and so on). Every inventory transaction updates it. On-hand calculations, availability checks, reservations and warehouse work all read it. For items enabled for warehouse management processing, the WHSInventReserve table stores the same on-hand data for each level of the reservation hierarchy.
Microsoft states that the performance of on-hand queries depends on the number of records in these tables. For this reason, it provides a periodic job, Warehouse management on-hand entries cleanup, that deletes records where all quantities are zero. Other useful links:
In practice, however, InventSum may keep growing even when the cleanup job runs every night, mostly because of closed records of old license plates. This post explains why the standard job can't delete them and provides a solution.
The following queries show how many closed records have accumulated. Results like these are a reason to investigate whether the standard cleanup is leaving old license plate records behind:
| Query | Records |
|---|---|
select count(*) from InventSum |
~10.2m |
select count(*) from InventSum where Closed = 1 |
~10.1m |
select count(*) from InventSum where Closed = 1 and LicensePlateId <> '' |
~10m |
select count(*) from WHSInventReserve |
~8m |
In this example, almost all InventSum records are closed, and almost all closed records have a license plate. Each license plate usually has only a couple of records, and most of these license plates were emptied months or even years ago. They are no longer used, yet the nightly cleanup job may run for hours without deleting their on-hand records.
Let's take one old license plate. It was emptied in 2024 and never used again:
| Item | Warehouse | Location | License plate | Closed | Modified |
|---|---|---|---|---|---|
| ITEM-A | WH1 | A-01-01 | LP-000123 | Yes | 2024-09-25 |
Location A-01-01 is a normal picking location, so other license plates come and go there every day:
| Item | Warehouse | Location | License plate | Closed | Modified |
|---|---|---|---|---|---|
| ITEM-A | WH1 | A-01-01 | LP-009871 | Yes | 2026-10-09 |
| ITEM-A | WH1 | A-01-01 | LP-009902 | No | 2026-10-10 |
| ITEM-A | WH1 | A-01-01 | LP-009915 | No | 2026-10-11 |
Let's run the standard Warehouse management on-hand entries cleanup just for this license plate: set License plate = LP-000123 in the filter and 30 in Delete if not updated for this many days. The job finishes almost instantly with the message:
"0 on-hand entries were deleted."
This isn't what we expect. The record is closed, all its quantities are zero, and it hasn't been updated for two years, so it should be deleted. Let's look at the standard code to see why this happens.
The job is implemented in the class InventCleanupWHSInventSumService. For every closed InventSum record that matches the filter, it calls two methods:
// InventCleanupWHSInventSumService.runCleanup()
this.cleanUpWHSInventReserve(inventSum, inventDim, inventTable);
if (this.validateWHSInventReserveRecsDeleted(inventTable, inventDim)
&& this.cleanUpInventSum(inventSum, inventDim))
{
cleanedUpInTransactionScope++;
}First, cleanUpWHSInventReserve() deletes the empty WHSInventReserve records of the item. It doesn't delete records whose on-hand record was updated within the cleanup period, so recently used license plates are kept, which is correct:
delete_from whsInventReserve
where whsInventReserve.ItemId == _inventTable.ItemId
&& whsInventReserve.AvailOrdered == 0
&& whsInventReserve.AvailPhysical == 0
&& whsInventReserve.ReservOrdered == 0
&& whsInventReserve.ReservPhysical == 0
...
notexists join inventSum
where inventSum.ItemId == whsInventReserve.ItemId
&& inventSum.InventDimId == whsInventReserve.InventDimId
&& inventSum.ModifiedDateTime > modifiedBefore;Then validateWHSInventReserveRecsDeleted() checks that no empty WHSInventReserve records are left. Only then is the InventSum record deleted:
private boolean validateWHSInventReserveRecsDeleted(InventTable _inventTable, InventDim _inventDim)
{
...
inventDimCriteria.data(_inventDim);
inventDimCriteria.clearFieldsFromHierarchyList(
WHSReservationHierarchyProvider::construct().
getDimListBelowLocation(_inventTable, WHSReservationHierarchySortOrder::BottomUp));
inventDimParm.initFromInventDim(inventDimCriteria);
select firstonly forceselectorder RecId from whsInventReserve
where whsInventReserve.ItemId == _inventTable.ItemId
&& (whsInventReserve.AvailOrdered == 0
&& whsInventReserve.AvailPhysical == 0
&& whsInventReserve.ReservOrdered == 0
&& whsInventReserve.ReservPhysical == 0
...)
&& #InventDimFieldsFilter(whsInventReserve, inventDimCriteria, inventDimParm)
exists join resHierarchyElement
where ...;
return whsInventReserve.RecId == 0;
}Note the clearFieldsFromHierarchyList() call. Before the check, the system clears all dimensions below the location. With a typical reservation hierarchy, the license plate is the lowest level, so it is below the location:
| Level | Dimension | Allow reservation on demand order |
|---|---|---|
| 1 | Site | Yes |
| 2 | Warehouse | Yes |
| 3 | Inventory status | Yes |
| 4 | Location | No |
| 5 | License plate | No |
So for LP-000123 the system doesn't check "are there any empty reservation records left for this license plate". Instead it checks "are there any empty reservation records left for ITEM-A at location A-01-01". For a busy location the answer is almost always yes. LP-009871 was emptied two days ago, so its empty WHSInventReserve record is still within the cleanup period and is correctly kept. Because of it, the old LP-000123 record isn't deleted either.
It's not clear why Microsoft implemented the check this way. Probably it covers some edge case for higher hierarchy levels, but it doesn't work when the license plate is the lowest level. The result is quite strange: if no license plate at a location was emptied recently, its old license plates are deleted, but at busy locations, where most of the records are, old license plates are kept forever. The job also checks the same records again every night, which may lead to it running for hours.
One possible option to make the standard job work is to swap the License plate and Location levels in the reservation hierarchy. However, this changes how reservations work for every item that uses the hierarchy, so a separate cleanup procedure is much safer.
This is the main part of this post: a custom cleanup class, DEV_CleanupDataTask_InventSumLicensePlate, that deletes exactly the records the standard job can't delete.
The idea is simple. If every on-hand record of a license plate is closed and older than the cleanup period, the license plate isn't used any more, and all its InventSum and WHSInventReserve records can be deleted. All other records are left to the standard job.
The logical flow is the following:
The class also provides the standard Estimate function of the cleanup module. Run it before the cleanup to see how many records will be deleted in each company.
The first run may take several hours, depending on the size of your tables. The next runs delete only the license plates that have expired since then.
The same note applies as for the standard job: once the license plate records are deleted, the On-hand list with closed transactions and the Physical inventory by inventory dimension report for past dates no longer show these license plates. Microsoft describes this in the cleanup job documentation and considers the performance gain more important. Choose the cleanup period with this in mind.
This class is part of the cleanup module I described for AX2009/AX2012 in Implementing Dynamics AX 2009/2012 database cleanup. The module has now been upgraded to D365FO:
cleanup() deletes the data and estimate() shows what will be deleted. The base class comment describes how to write each kind of task:
The standard Warehouse management on-hand entries cleanup can't delete closed on-hand records of a license plate when the license plate is the lowest level of the reservation hierarchy and its location is still in use. In a typical warehouse management implementation these are almost all of the closed records in InventSum.
This issue still exists in the current version, D365FO 10.0.49, and may be resolved in a future version.
In any case, the described class provides a faster way to remove them. It works with license plates instead of locations, doesn't touch license plates that are still in use, deletes in short set-based steps by the clustered index, and can be stopped and restarted at any time.
As usual, validate all queries in your own environment before running them in production. If you have questions or suggestions, feel free to contact me.
Similar posts: