From 6e321d3c56d7bd24773f92aa0bd56a2e0428108c Mon Sep 17 00:00:00 2001 From: Djordje Cenic Date: Sat, 22 Aug 2026 12:04:00 +0200 Subject: [PATCH 1/2] Correct severe misconception about SetCurrentKey in the knowledge base --- .../apply-filters-before-iterating.md | 2 +- ...currentkey-aligns-key-with-filters.good.al | 15 -------- .../setcurrentkey-aligns-key-with-filters.md | 24 ------------- ...tkey-sets-sort-order-not-index-hint.bad.al | 20 +++++++++++ ...key-sets-sort-order-not-index-hint.good.al | 18 ++++++++++ ...rrentkey-sets-sort-order-not-index-hint.md | 35 +++++++++++++++++++ 6 files changed, 74 insertions(+), 40 deletions(-) delete mode 100644 microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.good.al delete mode 100644 microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.md create mode 100644 microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.bad.al create mode 100644 microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.good.al create mode 100644 microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.md diff --git a/microsoft/knowledge/performance/apply-filters-before-iterating.md b/microsoft/knowledge/performance/apply-filters-before-iterating.md index 5f54c3b..76d78ec 100644 --- a/microsoft/knowledge/performance/apply-filters-before-iterating.md +++ b/microsoft/knowledge/performance/apply-filters-before-iterating.md @@ -15,7 +15,7 @@ A `SetRange` or `SetFilter` placed before `FindSet` narrows the result set at th ## Best Practice -Move every predicate that can be expressed as an equality or range filter into a `SetRange` or `SetFilter` ahead of the find. Combine with `SetCurrentKey` to choose a key whose first fields match the filter (see `setcurrentkey-aligns-key-with-filters.md`). The loop body should then contain only the work that depends on per-row state. +Move every predicate that can be expressed as an equality or range filter into a `SetRange` or `SetFilter` ahead of the find. Make sure a key (index) exists whose leading fields cover the filter so the optimizer can seek; note that `SetCurrentKey` only sets sort order and is not an index hint (see `setcurrentkey-sets-sort-order-not-index-hint.md`). The loop body should then contain only the work that depends on per-row state. See sample: `apply-filters-before-iterating.good.al`. diff --git a/microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.good.al b/microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.good.al deleted file mode 100644 index 4ab3b0a..0000000 --- a/microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.good.al +++ /dev/null @@ -1,15 +0,0 @@ -codeunit 50230 "Perf Sample SetCurrentKey Good" -{ - procedure ProcessLines(var SalesHeader: Record "Sales Header") - var - SalesLine: Record "Sales Line"; - begin - SalesLine.SetCurrentKey("Document Type", "Document No.", "Line No."); - SalesLine.SetRange("Document Type", SalesHeader."Document Type"); - SalesLine.SetRange("Document No.", SalesHeader."No."); - if SalesLine.FindSet() then - repeat - // ... - until SalesLine.Next() = 0; - end; -} diff --git a/microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.md b/microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.md deleted file mode 100644 index f7f8180..0000000 --- a/microsoft/knowledge/performance/setcurrentkey-aligns-key-with-filters.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -bc-version: [all] -domain: performance -keywords: [setcurrentkey, key, index, filter, sort] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Pick a key whose fields cover the filter and sort with SetCurrentKey - -## Description - -The platform chooses a key for each record access. When the filters or required sort do not match the primary key — or any non-explicit choice — the query may run against a key that does not cover the filter columns. Per the upstream guidance, "Use `SetCurrentKey()` to select the most efficient key for your filters" and "match key fields to your filter/sort requirements." Filtering on fields that are not in any key is flagged as bad — there is no index to ride and the access ends up reading more than necessary. - -## Best Practice - -When the access pattern is anything other than primary-key lookup, look at the filters and the desired sort, then either pick an existing key whose leading fields cover them and call `SetCurrentKey(...)`, or declare a new key on the table for the pattern. Match leading fields first — a key starting with `"Document Type", "Document No.", "Line No."` serves a filter on those three; a key starting with `"Line No."` does not. - -See sample: `setcurrentkey-aligns-key-with-filters.good.al`. - -## Anti Pattern - -Applying filters on fields that no key indexes, leaving the platform to read more than it should. The query produces the right answer; the cost surfaces only at production volume. The mirror case is forgetting `SetCurrentKey` when the wanted sort differs from the primary key — the iteration may then be sorted in memory after a wider read than necessary. diff --git a/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.bad.al b/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.bad.al new file mode 100644 index 0000000..776994a --- /dev/null +++ b/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.bad.al @@ -0,0 +1,20 @@ +codeunit 50231 "Perf Sample SetCurrentKey Bad" +{ + // Misconception: SetCurrentKey does NOT tell SQL Server to use this index. + // The optimizer picks the index from the filters (WHERE clause) and statistics. + // The result order is never used here, so SetCurrentKey only adds an ORDER BY + // the query does not need — and can push the plan toward a sort. + procedure SumRemainingAmount(CustomerNo: Code[20]) Total: Decimal + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + CustLedgerEntry.SetCurrentKey("Customer No.", Open, "Posting Date"); + CustLedgerEntry.SetRange("Customer No.", CustomerNo); + CustLedgerEntry.SetRange(Open, true); + CustLedgerEntry.SetAutoCalcFields("Remaining Amount"); + if CustLedgerEntry.FindSet() then + repeat + Total += CustLedgerEntry."Remaining Amount"; + until CustLedgerEntry.Next() = 0; + end; +} diff --git a/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.good.al b/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.good.al new file mode 100644 index 0000000..a4fc43f --- /dev/null +++ b/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.good.al @@ -0,0 +1,18 @@ +codeunit 50230 "Perf Sample SetCurrentKey Good" +{ + // SetCurrentKey is used because the rows must be processed oldest-first. + // The sort is a functional requirement, so the ORDER BY it adds is justified. + procedure ApplyOldestEntriesFirst(CustomerNo: Code[20]) + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + CustLedgerEntry.SetRange("Customer No.", CustomerNo); + CustLedgerEntry.SetRange(Open, true); + CustLedgerEntry.SetCurrentKey("Posting Date"); + if CustLedgerEntry.FindSet() then + repeat + // Apply entries in posting-date order ... + until CustLedgerEntry.Next() = 0; + end; +} + diff --git a/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.md b/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.md new file mode 100644 index 0000000..40428ca --- /dev/null +++ b/microsoft/knowledge/performance/setcurrentkey-sets-sort-order-not-index-hint.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: performance +keywords: [setcurrentkey, sort, order-by, index, key, query-optimizer, hint] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# SetCurrentKey only sets sort order — it is not an index hint + +## Description + +A common misconception is that `SetCurrentKey` tells SQL Server which index to use for a query. It does not. In Business Central, `SetCurrentKey` only changes the `ORDER BY` clause of the generated SQL statement. It does not add an index hint, and the SQL Server query optimizer is free to ignore the named key entirely. + +The optimizer picks the index from the `WHERE` clause (your `SetRange`/`SetFilter`) together with table statistics and estimated cost. In practice it almost never chooses an index just because that key appears in `ORDER BY`. So calling `SetCurrentKey` to "steer" the plan toward an index is a no-op for index selection — and can make things worse: an `ORDER BY` that the query does not otherwise need can push the optimizer toward a less selective index or add a Sort operator to the plan. + +Selectivity comes from having the right index available (a key on the table whose leading fields cover the filter) and from filtering on those fields — not from `SetCurrentKey`. + +## Best Practice + +Decide `SetCurrentKey` on one question only: **do I need the result set in a specific order?** + +- If yes — you iterate rows in a defined sequence, or rely on `FindFirst`/`FindLast`/`Next` returning a particular row — call `SetCurrentKey` for that sort. The order is a functional requirement, and the `ORDER BY` is justified. +- If no — omit `SetCurrentKey`. Let the optimizer choose the cheapest plan for your filters; it may pick a better index and skip a sort. + +To make a filtered read fast, ensure a key (index) exists on the table whose leading fields cover the filter, and filter on those fields with `SetRange`/`SetFilter`. That is what lets the optimizer seek. Defining the key creates the index; `SetCurrentKey` is not required to make the optimizer use it. + +See sample: `setcurrentkey-sets-sort-order-not-index-hint.good.al`. + +## Anti Pattern + +Adding `SetCurrentKey` to a filtered read purely in the belief that it forces SQL Server to seek a particular index, when the code never uses the resulting order. This does nothing for index selection and only appends an `ORDER BY` the query does not need, risking an unnecessary sort. Remove the `SetCurrentKey`; rely on the filters and an existing covering key instead. + +See sample: `setcurrentkey-sets-sort-order-not-index-hint.bad.al`. From 9fab60153f868536e63c9d62626293197288ac94 Mon Sep 17 00:00:00 2001 From: Yahya Touil <60827484+yahyatouil-dev@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:54:37 +0100 Subject: [PATCH 2/2] Add TransferFields SkipFieldsNotMatchingType guidance (#133) * Add TransferFields SkipFieldsNotMatchingType guidance * Update transferfields-skip-type-mismatch-can-drop-data.md * Update transferfields-skip-type-mismatch-can-drop-data.good.al * Move good sample reference under Best Practice Aligns the article with the repo convention used by the sibling data-modeling files: the .good.al reference belongs under Best Practice and the .bad.al reference under Anti Pattern. Previously both pointers sat under Anti Pattern, leaving the good-sample reference orphaned in the wrong section. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c998c71-f30b-40f3-b714-87fafed505d8 --------- Co-authored-by: Jesper Schulz Copilot-Session: 3c998c71-f30b-40f3-b714-87fafed505d8 --- ...ds-skip-type-mismatch-can-drop-data.bad.al | 55 +++++++++++++++++ ...s-skip-type-mismatch-can-drop-data.good.al | 59 +++++++++++++++++++ ...fields-skip-type-mismatch-can-drop-data.md | 26 ++++++++ 3 files changed, 140 insertions(+) create mode 100644 community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.bad.al create mode 100644 community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.good.al create mode 100644 community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.md diff --git a/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.bad.al b/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.bad.al new file mode 100644 index 0000000..691dfd2 --- /dev/null +++ b/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.bad.al @@ -0,0 +1,55 @@ +table 50123 "Transfer Source Bad" +{ + fields + { + field(1; "Entry No."; Integer) + { + DataClassification = CustomerContent; + } + field(2; "Reference"; Code[20]) + { + DataClassification = CustomerContent; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } + +} + +table 50124 "Transfer Target Bad" +{ + fields + { + field(1; "Entry No."; Integer) + { + DataClassification = CustomerContent; + } + field(2; "Reference"; Integer) + { + DataClassification = CustomerContent; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } + +} + +codeunit 50492 "TransferFields Bad" +{ + procedure CopyData(Source: Record "Transfer Source Bad"; var Target: Record "Transfer Target Bad") + begin + Target.TransferFields(Source, true, true); + end; +} \ No newline at end of file diff --git a/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.good.al b/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.good.al new file mode 100644 index 0000000..a0ac2a8 --- /dev/null +++ b/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.good.al @@ -0,0 +1,59 @@ +table 50121 "Transfer Source" +{ + fields + { + field(1; "Entry No."; Integer) + { + DataClassification = CustomerContent; + } + field(2; "Reference"; Code[20]) + { + DataClassification = CustomerContent; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } + +} + +table 50122 "Transfer Target" +{ + fields + { + field(1; "Entry No."; Integer) + { + DataClassification = CustomerContent; + } + field(2; "Reference"; Integer) + { + DataClassification = CustomerContent; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } + +} + +codeunit 50491 "TransferFields Good" +{ + procedure CopyData(Source: Record "Transfer Source"; var Target: Record "Transfer Target") + var + ConvertedReference: Integer; + begin + Target."Entry No." := Source."Entry No."; + Evaluate(ConvertedReference, Source."Reference"); + Target.Validate("Reference", ConvertedReference); + end; +} diff --git a/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.md b/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.md new file mode 100644 index 0000000..7d1ea77 --- /dev/null +++ b/community/knowledge/data-modeling/transferfields-skip-type-mismatch-can-drop-data.md @@ -0,0 +1,26 @@ +--- +bc-version: [16..] +domain: data-modeling +keywords: [transferfields, skipfieldsnotmatchingtype, type-mismatch, field-mapping, data-transfer] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Do not use SkipFieldsNotMatchingType to hide required TransferFields mismatches + +## Description + +`Record.TransferFields` copies values between fields with matching field numbers. Without `SkipFieldsNotMatchingType` (or with it `false`), a type mismatch between two fields in the same extension raises a runtime error at the point of transfer. Setting `SkipFieldsNotMatchingType` to `true` removes that error: the field is skipped instead, and the rest of the transfer completes normally. The caller gets no indication that a field was not copied. + +## Best Practice + +Use `TransferFields(Source)` only when every field the destination requires, including primary key fields, is guaranteed to share a matching field number and type with the source; this form defaults `InitPrimaryKeyFields` to `true`. Fields with no matching field number, and fields whose types differ across extensions, are skipped regardless of `SkipFieldsNotMatchingType` — that parameter only governs same-extension type mismatches. If the destination depends on a field that falls into either case, map and validate it explicitly in code rather than relying on `TransferFields` to catch the gap. Use `SkipFieldsNotMatchingType = true` only when skipping same-extension type mismatches is an intentional, documented part of the transfer contract. + +See sample: `transferfields-skip-type-mismatch-can-drop-data.good.al`. + +## Anti Pattern + +Using `TransferFields(Source, InitPrimaryKeyFields, true)` as a generic way to make two evolving table schemas transfer without errors, when the destination depends on every required source field being copied. A type change on either table can turn a previously transferred field into a silently skipped one without making the transfer itself fail. + +See sample: `transferfields-skip-type-mismatch-can-drop-data.bad.al`. \ No newline at end of file