diff --git a/microsoft/knowledge/performance/avoid-commit-inside-loops.good.al b/microsoft/knowledge/performance/avoid-commit-inside-loops.good.al index e8b55b2..5c22c58 100644 --- a/microsoft/knowledge/performance/avoid-commit-inside-loops.good.al +++ b/microsoft/knowledge/performance/avoid-commit-inside-loops.good.al @@ -1,3 +1,17 @@ +query 50127 "Perf Customer Chunk" +{ + QueryType = Normal; + OrderBy = ascending(CustomerNo); + + elements + { + dataitem(Customer; Customer) + { + column(CustomerNo; "No.") { } + } + } +} + codeunit 50128 "Perf Sample CommitInLoop Good" { procedure NormalizeCustomerNames() @@ -5,28 +19,45 @@ codeunit 50128 "Perf Sample CommitInLoop Good" LastCustomerNo: Code[20]; begin // The outer loop owns checkpoints; the per-row loop contains no Commit. - while NormalizeNextChunk(LastCustomerNo, 500) do + while NormalizeNextChunk(LastCustomerNo) do Commit(); end; - local procedure NormalizeNextChunk(var LastCustomerNo: Code[20]; ChunkSize: Integer): Boolean + local procedure NormalizeNextChunk(var LastCustomerNo: Code[20]): Boolean var Customer: Record Customer; - RowsInChunk: Integer; + CustomerChunk: Query "Perf Customer Chunk"; + FirstCustomerNo: Code[20]; + LastChunkCustomerNo: Code[20]; begin - Customer.SetCurrentKey("No."); + CustomerChunk.TopNumberOfRows(500); if LastCustomerNo <> '' then - Customer.SetFilter("No.", '>%1', LastCustomerNo); - if not Customer.FindSet(true) then + CustomerChunk.SetFilter(CustomerNo, '>%1', LastCustomerNo); + CustomerChunk.Open(); + if not CustomerChunk.Read() then begin + CustomerChunk.Close(); exit(false); + end; + + FirstCustomerNo := CustomerChunk.CustomerNo; + repeat + LastChunkCustomerNo := CustomerChunk.CustomerNo; + until not CustomerChunk.Read(); + CustomerChunk.Close(); + + Customer.SetCurrentKey("No."); + Customer.SetRange("No.", FirstCustomerNo, LastChunkCustomerNo); + if not Customer.FindSet(true) then begin + LastCustomerNo := LastChunkCustomerNo; + exit(true); + end; repeat Customer.Name := UpperCase(Customer.Name); Customer.Modify(); - LastCustomerNo := Customer."No."; - RowsInChunk += 1; - until (RowsInChunk >= ChunkSize) or (Customer.Next() = 0); + until Customer.Next() = 0; + LastCustomerNo := LastChunkCustomerNo; exit(true); end; } diff --git a/microsoft/knowledge/performance/avoid-commit-inside-loops.md b/microsoft/knowledge/performance/avoid-commit-inside-loops.md index 4dd5b11..10e888e 100644 --- a/microsoft/knowledge/performance/avoid-commit-inside-loops.md +++ b/microsoft/knowledge/performance/avoid-commit-inside-loops.md @@ -1,7 +1,7 @@ --- bc-version: [all] domain: performance -keywords: [commit, loop, transaction, lock, checkpoint, codeunit-run] +keywords: [commit, loop, transaction, lock, checkpoint, bounded, watermark, topnumberofrows, codeunit-run] technologies: [al] countries: [w1] application-area: [all] @@ -13,11 +13,11 @@ 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 process 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 each retrieve and process the next N rows. ## Best Practice -If the batch is large enough that a single transaction is untenable, use an outer loop that selects and finishes the next N rows. Commit only after the inner row loop has returned and the checkpoint state identifies where the next chunk starts. 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 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`. See sample: `avoid-commit-inside-loops.good.al`.