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) <noreply@anthropic.com>
This commit is contained in:
Jeremy Vyska 2026-07-01 11:21:28 +02:00
parent f5156c61de
commit d45068969a
6 changed files with 74 additions and 62 deletions

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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.

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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.