Let's simplify publishing new NuGet packages for x++ builds
N + 1 query pattern
What is an N + 1 query?
An N + 1 query is an instance where a query contains another query - Get orders then get order lines. Its fairly common in legacy F&O code and is generally (now) considered bad practice to use. But seeing vs using it can be somewhat confusing. First, let's review.
Classic ( or Legacy ) - selects in loops (N+1 query pattern)
Below is an example of what this may look like:
while select salesLine
where salesLine.SalesId == _salesId
{select firstOnly inventSum
where inventSum.ItemId == salesLine.ItemId
&& inventSum.InventDimId == salesLine.InventDimId;// do something
}
We're getting data to get more data. This can, and does, kill performance. We want to ask the database for all the data we need to act on in a single request whenever possible. Consider you're at a resturant and you're in a hurry. If you ask the waiter for a drink, a food selection from the money, say you're not having desert and to bring the check with the food, you're front loading all requests so the server won't keep coming back to ask for more instructions. We want to do something similar with the database. "here's what I want and here's how I want it". An example is below:
while select salesLine
join inventSum
where inventSum.ItemId == salesLine.ItemId
&& inventSum.InventDimId == salesLine.InventDimId
&& salesLine.SalesId == _salesId
{
// do something
}
If that's not an option, you can try doing some prefetch in a loop so you can process or analyze later. This would involve building a map with a key from a single query then looping on that without doing any additional selects, like so:
internal final class RunnableClass1
{
public static void main(Args _args)
{
SalesTable salesTable;
SalesLine salesLine;
InventSum inventSum;Map sumByItemDim = new Map(Types::Container, Types::Real);
container key;
real availPhys;//ie; focus on one sales order. replace as needed.
select firstOnly salesTable
where salesTable.SalesId == 'SO-000123';if (!salesTable.RecId)
{
info("Sales order not found");
return;
}//a single query builds the Map
while select ItemId, InventDimId, AvailPhysical
from inventSum
exists join salesLine
where salesLine.SalesId == salesTable.SalesId
&& salesLine.ItemId == inventSum.ItemId
&& salesLine.InventDimId == inventSum.InventDimId
{
key = [inventSum.ItemId, inventSum.InventDimId];if (sumByItemDim.exists(key))
{
sumByItemDim.insert(key, sumByItemDim.lookup(key) + inventSum.AvailPhysical);
}
else
{
sumByItemDim.insert(key, inventSum.AvailPhysical);
}
}//next, loop SalesLine with no additional selects
while select salesLine
where salesLine.SalesId == salesTable.SalesId
{
key = [salesLine.ItemId, salesLine.InventDimId];if (sumByItemDim.exists(key))
{
availPhys = sumByItemDim.lookup(key);
info(strFmt("Line %1: %2 / %3 => AvailPhys=%4",
salesLine.LineNum,
salesLine.ItemId,
salesLine.InventDimId,
availPhys));
}
else
{
//not found in map = treat as 0 / missing
info(strFmt("Line %1: %2 / %3 => AvailPhys not found (0)",
salesLine.LineNum,
salesLine.ItemId,
salesLine.InventDimId));
}
}
}}
With this, we're still hitting 2 data sources but we're not looping them one inside another. The looping of queries is what (typically) causes performance issues. Lastly, if you have to have a unique workload of need that does require some strange interactions with the data, create a batch job for it. we have several types of batch job types for different types of workloads.











