From d45068969a19c5a9402853f656cf5530ea3bd2f4 Mon Sep 17 00:00:00 2001 From: Jeremy Vyska Date: Wed, 1 Jul 2026 11:21:28 +0200 Subject: [PATCH] Correct SetLoadFields JIT-load article to match MS docs The draft claimed accessing an unlisted field "reloads the entire row" per record. Microsoft's partial-records docs say otherwise: the platform does an implicit Get that loads the missing field(s), and in a direct var loop the first JIT updates the enumerator so later iterations do not re-load. The genuine per-row penalty is the pass-by-value case, where the copy's enumerator is not updated. Rewrite the article around JIT loading and the by-value footgun, rename the slug from ...full-reload to ...jit-load, and fix the good/bad samples to demonstrate the by-value repetition accurately. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...unlisted-field-triggers-full-reload.bad.al | 20 -------------- ...nlisted-field-triggers-full-reload.good.al | 18 ------------- ...lds-unlisted-field-triggers-full-reload.md | 24 ----------------- ...ds-unlisted-field-triggers-jit-load.bad.al | 26 +++++++++++++++++++ ...s-unlisted-field-triggers-jit-load.good.al | 24 +++++++++++++++++ ...fields-unlisted-field-triggers-jit-load.md | 24 +++++++++++++++++ 6 files changed, 74 insertions(+), 62 deletions(-) delete mode 100644 community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.bad.al delete mode 100644 community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.good.al delete mode 100644 community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.md create mode 100644 community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.bad.al create mode 100644 community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.good.al create mode 100644 community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md diff --git a/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.bad.al b/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.bad.al deleted file mode 100644 index c724cbd..0000000 --- a/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.bad.al +++ /dev/null @@ -1,20 +0,0 @@ -codeunit 50132 "LoadFields Bad Sample" -{ - procedure TotalReleasedAmount(): Decimal - var - SalesHeader: Record "Sales Header"; - Total: Decimal; - begin - // "Currency Code" is not in the list. Reading it each iteration forces - // a second database round-trip that reloads the WHOLE row — N full - // reloads, slower than never calling SetLoadFields at all. - SalesHeader.SetLoadFields("Amount Including VAT", Status); - if SalesHeader.FindSet() then - repeat - if (SalesHeader.Status = SalesHeader.Status::Released) and - (SalesHeader."Currency Code" = '') then - Total += SalesHeader."Amount Including VAT"; - until SalesHeader.Next() = 0; - exit(Total); - end; -} diff --git a/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.good.al b/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.good.al deleted file mode 100644 index fdc19d8..0000000 --- a/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.good.al +++ /dev/null @@ -1,18 +0,0 @@ -codeunit 50132 "LoadFields Good Sample" -{ - procedure TotalReleasedAmount(): Decimal - var - SalesHeader: Record "Sales Header"; - Total: Decimal; - begin - // Every field read in the loop is listed, so each row stays a cheap - // partial load with no hidden second round-trip. - SalesHeader.SetLoadFields("Amount Including VAT", Status); - if SalesHeader.FindSet() then - repeat - if SalesHeader.Status = SalesHeader.Status::Released then - Total += SalesHeader."Amount Including VAT"; - until SalesHeader.Next() = 0; - exit(Total); - end; -} diff --git a/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.md b/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.md deleted file mode 100644 index c536442..0000000 --- a/community/knowledge/performance/setloadfields-unlisted-field-triggers-full-reload.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -bc-version: [all] -domain: performance -keywords: [setloadfields, partial-records, just-in-time-load, field-reload, round-trip, lazy-load] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Touching an unlisted field after SetLoadFields forces a full-row reload - -> Contributions welcome — open a PR to refine or extend this article. - -## Description - -`SetLoadFields` loads only the named fields, but the trap is what happens when code later reads a field that was *not* listed: the platform silently issues a second database round-trip and reloads the **entire row** for that record — per record. In a loop, a single overlooked field turns one cheap partial read into N full-row reloads, which is slower than never calling `SetLoadFields` at all. The optimization is only a win if the listed set covers every field touched anywhere downstream, not just in the immediate code block. - -## Best Practice - -Before adding `SetLoadFields`, audit the *whole* access lifecycle of the record variable — every field read in the loop body, in called procedures, in `OnValidate`/`OnAfterGetRecord`, and in anything that receives the record by reference — and list all of them. If you cannot enumerate them confidently (for example the record is passed to code you do not control), prefer not to call `SetLoadFields` rather than risk the reload penalty. See the existing guidance on when partial records pay off (`use-setloadfields-for-partial-records`). - -## Anti Pattern - -Adding `SetLoadFields(Field1, Field2)` at the top of a loop, then reading `Field3` deeper in the body or in a helper. The code compiles and returns correct data, but each iteration pays a hidden full-row reload — the change reads as an optimization while regressing performance. Reviewer signal: a `SetLoadFields` list that does not include every field subsequently referenced through that record variable. diff --git a/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.bad.al b/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.bad.al new file mode 100644 index 0000000..a5c3036 --- /dev/null +++ b/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.bad.al @@ -0,0 +1,26 @@ +codeunit 50132 "LoadFields Bad Sample" +{ + procedure TotalReleasedAmount(): Decimal + var + SalesHeader: Record "Sales Header"; + Total: Decimal; + begin + // "Currency Code" is not listed. The helper takes SalesHeader BY VALUE, + // so the copy neither shares the load set nor updates the enumerator: + // reading the unlisted field triggers a fresh JIT load (an extra Get) + // on EVERY iteration, quietly reversing the saving. + SalesHeader.SetLoadFields("Amount Including VAT", Status); + if SalesHeader.FindSet() then + repeat + if IsLocalReleased(SalesHeader) then + Total += SalesHeader."Amount Including VAT"; + until SalesHeader.Next() = 0; + exit(Total); + end; + + local procedure IsLocalReleased(SalesHeader: Record "Sales Header"): Boolean + begin + exit((SalesHeader.Status = SalesHeader.Status::Released) and + (SalesHeader."Currency Code" = '')); + end; +} diff --git a/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.good.al b/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.good.al new file mode 100644 index 0000000..7ab9016 --- /dev/null +++ b/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.good.al @@ -0,0 +1,24 @@ +codeunit 50132 "LoadFields Good Sample" +{ + procedure TotalReleasedAmount(): Decimal + var + SalesHeader: Record "Sales Header"; + Total: Decimal; + begin + // Every field read anywhere downstream is listed — including the one + // the by-var helper reads — so no JIT load is ever triggered. + SalesHeader.SetLoadFields("Amount Including VAT", Status, "Currency Code"); + if SalesHeader.FindSet() then + repeat + if IsLocalReleased(SalesHeader) then + Total += SalesHeader."Amount Including VAT"; + until SalesHeader.Next() = 0; + exit(Total); + end; + + local procedure IsLocalReleased(var SalesHeader: Record "Sales Header"): Boolean + begin + exit((SalesHeader.Status = SalesHeader.Status::Released) and + (SalesHeader."Currency Code" = '')); + end; +} diff --git a/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md b/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md new file mode 100644 index 0000000..ddce795 --- /dev/null +++ b/community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md @@ -0,0 +1,24 @@ +--- +bc-version: [all] +domain: performance +keywords: [setloadfields, partial-records, just-in-time-load, jit-load, round-trip, pass-by-value, enumerator] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Reading an unlisted field after SetLoadFields triggers a JIT load + +> Contributions welcome — open a PR to refine or extend this article. + +## Description + +`SetLoadFields` loads only the named fields, but the trap is what happens when code later reads a field that was *not* listed: the platform silently issues a **just-in-time (JIT) load** — an implicit `Get` that fetches the missing field(s) in a second database round-trip. A single JIT load can erase the saving; the real danger is a JIT that repeats per record. The optimization is only a win if the listed set covers every field touched anywhere downstream, not just in the immediate code block. + +## Best Practice + +Before adding `SetLoadFields`, audit the *whole* access lifecycle of the record variable — every field read in the loop body, in called procedures, in `OnValidate`/`OnAfterGetRecord`, and in anything that receives the record — and list all of them via `SetLoadFields`/`AddLoadFields`. Be especially careful when passing a partial record **by value**: the copy does not share the load set and its enumerator is not updated, so a helper that reads an unlisted field re-triggers the JIT on *every* iteration. Pass by `var` where you can (a JIT then updates the enumerator, so later iterations don't re-load), or call `AddLoadFields` before passing by value. If you cannot enumerate the fields confidently, prefer not to call `SetLoadFields` at all. See the existing guidance on when partial records pay off (`use-setloadfields-for-partial-records`). + +## Anti Pattern + +Adding `SetLoadFields(Field1, Field2)` at the top of a loop, then reading `Field3` deeper in the body or inside a by-value helper. The code compiles and returns correct data, but pays a hidden JIT round-trip — and in the by-value case it repeats once per row, quietly reversing the gain. JIT loads also introduce `Inconsistent read` / record-modified race errors that a full non-partial load avoids. Reviewer signal: a `SetLoadFields` list that omits a field later read through that record variable, especially a record passed by value to a procedure that reads a field the caller never listed.