Address performance retrieval review

Retrieve Commit-in-loop guidance precisely, process exact checkpoint key lists, and narrow clone-before-write discovery.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 85be3fc4-5253-47b8-ba1b-6b8fd188fcea
This commit is contained in:
Jesper Schulz-Wedde 2026-07-14 11:59:19 +02:00
parent e374a8a4b8
commit 737fbe5cd3
4 changed files with 27 additions and 27 deletions

View file

@ -1,7 +1,7 @@
---
bc-version: [all]
domain: performance
keywords: [clone, copy, recordref, gettable, by-value, modify, delete, loop]
keywords: [clone, clone-before-write, copy, gettable, by-value, copied-record, writing-helper]
technologies: [al]
countries: [w1]
application-area: [all]

View file

@ -26,36 +26,33 @@ codeunit 50128 "Perf Sample CommitInLoop Good"
local procedure NormalizeNextChunk(var LastCustomerNo: Code[20]): Boolean
var
Customer: Record Customer;
TempCustomer: Record Customer temporary;
CustomerChunk: Query "Perf Customer Chunk";
FirstCustomerNo: Code[20];
LastChunkCustomerNo: Code[20];
begin
CustomerChunk.TopNumberOfRows(500);
if LastCustomerNo <> '' then
CustomerChunk.SetFilter(CustomerNo, '>%1', LastCustomerNo);
CustomerChunk.Open();
if not CustomerChunk.Read() then begin
CustomerChunk.Close();
exit(false);
end;
FirstCustomerNo := CustomerChunk.CustomerNo;
repeat
while CustomerChunk.Read() do begin
TempCustomer.Init();
TempCustomer."No." := CustomerChunk.CustomerNo;
TempCustomer.Insert();
LastChunkCustomerNo := CustomerChunk.CustomerNo;
until not CustomerChunk.Read();
end;
CustomerChunk.Close();
Customer.SetCurrentKey("No.");
Customer.SetRange("No.", FirstCustomerNo, LastChunkCustomerNo);
if not Customer.FindSet(true) then begin
LastCustomerNo := LastChunkCustomerNo;
exit(true);
end;
if TempCustomer.IsEmpty() then
exit(false);
repeat
Customer.Name := UpperCase(Customer.Name);
Customer.Modify();
until Customer.Next() = 0;
Customer.LockTable();
if TempCustomer.FindSet() then
repeat
if Customer.Get(TempCustomer."No.") then begin
Customer.Name := UpperCase(Customer.Name);
Customer.Modify();
end;
until TempCustomer.Next() = 0;
LastCustomerNo := LastChunkCustomerNo;
exit(true);

View file

@ -1,7 +1,7 @@
---
bc-version: [all]
domain: performance
keywords: [commit, loop, transaction, lock, checkpoint, bounded, watermark, topnumberofrows, codeunit-run]
keywords: [commit, commit-in-loop, per-row-commit, checkpoint, bounded-checkpoint, watermark, topnumberofrows]
technologies: [al]
countries: [w1]
application-area: [all]
@ -13,16 +13,16 @@ application-area: [all]
## Description
Commit ends the current write transaction. Calling it inside a per-row loop produces one transaction per iteration and loses the ability to roll back the whole operation atomically; it also interferes with the platform's ability to batch write operations. Most loops need no explicit Commit at all — AL auto-commits the enclosing code module on successful completion (see `understand-implicit-transaction-boundary.md`). When the batch is too large for one transaction, the fix is not a per-row Commit but bounded checkpoints that each retrieve and process the next N rows.
Commit ends the current write transaction. Calling it inside a per-row loop produces one transaction per iteration and loses the ability to roll back the whole operation atomically; it also interferes with the platform's ability to batch write operations. Most loops need no explicit Commit at all — AL auto-commits the enclosing code module on successful completion (see `understand-implicit-transaction-boundary.md`). When the batch is too large for one transaction, the fix is not a per-row Commit but bounded checkpoints that select an exact list of at most N keys and process only those rows.
## Best Practice
If the batch is large enough that a single transaction is untenable, use an ordered primary-key watermark and retrieve a bounded next-N window. `FindSet` is optimized for reading the complete filtered set and isn't implemented as `TOP X`, so calling it over the remaining tail and breaking after N rows does not bound retrieval. The sample uses a query capped by [`TopNumberOfRows`](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/query/queryinstance-topnumberofrows-method) to discover the next upper key, then locks and processes only that key range. Commit after the bounded inner loop returns and persist its upper key as the next watermark. Use a stable key and define how a later run handles records inserted at or below an already committed watermark. A `Codeunit.Run` boundary can also own a chunk when its implicit commit and error behavior fit the caller — see `codeunit-run-as-atomic-sub-operation.md`.
If the batch is large enough that a single transaction is untenable, use an ordered primary-key watermark and retrieve a bounded next-N key list. `FindSet` is optimized for reading the complete filtered set and isn't implemented as `TOP X`, so calling it over the remaining tail and breaking after N rows does not bound retrieval. The sample uses a query capped by [`TopNumberOfRows`](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/query/queryinstance-topnumberofrows-method) to fill a temporary key buffer, then takes update locks and modifies only those exact keys. It does not reconstruct an inclusive first-to-last range that concurrent inserts could expand. Commit after the bounded inner loop returns and persist its last selected key as the next watermark. Use a stable key and define how a later run handles records inserted at or below an already committed watermark. A `Codeunit.Run` boundary can also own a chunk when its implicit commit and error behavior fit the caller — see `codeunit-run-as-atomic-sub-operation.md`.
See sample: `avoid-commit-inside-loops.good.al`.
## Anti Pattern
Placing Commit inside `repeat ... until Next() = 0` is almost always a mistake: it is unusual for the correctness of the operation to depend on per-row commits, and the cost of starting a new transaction on every row dominates the work.
Placing Commit inside `repeat ... until Next() = 0` is almost always a mistake: it is unusual for the correctness of the operation to depend on per-row commits, and the cost of starting a new transaction on every row dominates the work. A capped query that discovers only an upper key and then re-reads an inclusive key range is not exact batching either; concurrent inserts inside that range can enlarge the checkpoint.
See sample: `avoid-commit-inside-loops.bad.al`.