mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
Seed performance knowledge corpus (22 articles + AL samples)
Converts an existing performance-review prompt into 22 atomic knowledge articles under microsoft/knowledge/performance/, each paired with AL samples under samples/performance/<slug>/ demonstrating the anti-pattern and/or the best practice. The full set seeds the corpus the microsoft/skills/al-performance-review leaf skill matches against and validates the READ knowledge-file format end-to-end. Every article conforms to the READ contract: six required frontmatter fields, Description always present, no fenced code blocks, sample code referenced by repo-relative path. Each article is marked with a blockquote 'Seed article' note so domain stewards can extend or restructure them freely. Articles (ordered by concern area): Database query efficiency - use-findset-with-next (AA0181) - avoid-findfirst-with-next (AA0233) - only-fetch-records-you-use (AA0175) - use-findset-readonly-by-default - use-setloadfields-for-partial-records - use-addloadfields-in-report-layouts - use-calcsums-to-aggregate-filtered-sets (file: use-calcsums-for-flowfield-totals.md) - avoid-calcfields-in-loops - add-sift-keys-for-flowfields (AA0232) - use-isempty-for-existence-checks Filter and key optimization - filter-before-find - set-current-key-to-match-filters Temporary tables and transactions - use-temporary-tables-for-intermediate-data - keep-transaction-scope-short - avoid-user-interaction-in-transactions - avoid-commit-inside-loops Record operations - prefer-get-for-primary-key-lookups - use-insert-false-when-skipping-triggers - prefer-direct-record-over-recordref Strings, codeunits, events - use-strsubstno-for-message-formatting - use-single-instance-codeunits-for-caching - keep-event-subscribers-lightweight samples/README.md documents the sample-folder convention and makes clear the samples are demonstration-only, not derived from BC base application source, with unique object IDs in the 50100-50199 range. Rubber-duck pass caught: a misleading good.al in avoid-calcfields-in-loops (fixed by switching to a hoistable CalcFields scenario), an invalid event subscriber signature in keep-event-subscribers-lightweight (fixed by adding var xRec), normative guidance leaked into the Description of use-findset-readonly-by-default (moved to Anti Pattern), a missing sample pair for keep-transaction-scope-short (added), and muddy FlowField/CalcSums framing (retitled and clarified). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
5aaa58e8ee
commit
32c40bbf1d
58 changed files with 1110 additions and 0 deletions
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [sift, sumindexfields, flowfield, key, aa0232]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Add SIFT keys for FlowField aggregations
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop rule AA0232 checks that FlowFields backed by CalcSums or aggregation CalcFormula are supported by a key whose SumIndexFields include the summed field and whose key prefix matches the formula's filter fields. Without a SIFT key the platform falls back to a full aggregation on every read — typically invisible in development and catastrophic in production.
|
||||
|
||||
## Best Practice
|
||||
|
||||
For each Sum-style FlowField, ensure the source table has a key whose leading fields match the FlowField's CalcFormula WHERE clause and whose SumIndexFields list includes the summed field. Table extensions adding new FlowFields are responsible for adding the supporting key.
|
||||
|
||||
See sample: `samples/performance/add-sift-keys-for-flowfields/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Declaring a FlowField on a hot table without checking whether a supporting SIFT key exists ships a latent scan into every list page and report that touches the field.
|
||||
|
||||
29
microsoft/knowledge/performance/avoid-calcfields-in-loops.md
Normal file
29
microsoft/knowledge/performance/avoid-calcfields-in-loops.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [calcfields, flowfield, loop, n-plus-one]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Do not call CalcFields inside loops
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
CalcFields evaluates one or more FlowFields for the current record by issuing a separate SQL aggregation. Called inside a loop over a record set, it becomes an N+1 problem: one aggregate per row. For any non-trivial set on a ledger-entry-backed FlowField this is orders of magnitude slower than the equivalent batched query.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Move CalcFields out of the iteration. If the total is what you need, use CalcSums on the filtered parent set. If row-by-row FlowField values are needed, reshape the computation so the aggregate runs once — for example by joining against a temporary table populated in a single batched query.
|
||||
|
||||
See sample: `samples/performance/avoid-calcfields-in-loops/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling CalcFields inside `repeat ... until Next() = 0` on a hot parent record is the textbook N+1 pattern. Even a modest parent set size (hundreds of rows) turns into thousands of round-trips.
|
||||
|
||||
See sample: `samples/performance/avoid-calcfields-in-loops/bad.al`.
|
||||
|
||||
27
microsoft/knowledge/performance/avoid-commit-inside-loops.md
Normal file
27
microsoft/knowledge/performance/avoid-commit-inside-loops.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [commit, loop, transaction, lock]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Do not Commit inside loops
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Commit ends the current transaction. Calling it inside a 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. The original motivation — releasing locks during a long batch — is better served by splitting the batch into explicit checkpoints that each process a bounded number of rows.
|
||||
|
||||
## Best Practice
|
||||
|
||||
If the batch is large enough that a single transaction is untenable, process it in checkpoints driven by an outer loop that each time picks up the next N rows. Commit once per checkpoint at a clearly defined safe boundary, not inside the per-row loop.
|
||||
|
||||
## 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.
|
||||
|
||||
See sample: `samples/performance/avoid-commit-inside-loops/bad.al`.
|
||||
|
||||
27
microsoft/knowledge/performance/avoid-findfirst-with-next.md
Normal file
27
microsoft/knowledge/performance/avoid-findfirst-with-next.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [findfirst, findlast, get, next, aa0233]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Do not pair FindFirst, FindLast, or Get with Next
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop rule AA0233 flags loops that start with FindFirst, FindLast, or Get and then call Next. FindFirst and FindLast retrieve a single row and reposition the cursor; calling Next after them forces the platform to re-seek and stream the rest of the set, which is slower than the correct FindSet pattern and signals intent incorrectly to reviewers and the optimizer.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Choose the Find variant that matches the operation: FindSet for full iteration, FindFirst or FindLast when you want exactly one row, Get when the primary key is known. Never call Next after FindFirst, FindLast, or Get.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Writing `if Rec.FindFirst() then repeat ... until Rec.Next() = 0` is the canonical AA0233 offender. The loop wastes bandwidth and obscures the author's intent.
|
||||
|
||||
See sample: `samples/performance/avoid-findfirst-with-next/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [confirm, strmenu, message, transaction, dialog]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Do not prompt the user inside a write transaction
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Confirm, StrMenu, Message, and any other user-facing dialog pauses execution while the transaction is still open. During that pause every lock held by the transaction blocks other sessions. A user who walks away from the screen can suspend business-critical tables for an unbounded period.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Gather every user decision before the writing phase begins. Once the decisions are known, run the transaction end-to-end without prompts.
|
||||
|
||||
See sample: `samples/performance/avoid-user-interaction-in-transactions/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling Confirm or StrMenu from inside an OnInsert, OnModify, or OnDelete trigger — or from any code path that has already started modifying records — blocks on user input while holding locks.
|
||||
|
||||
See sample: `samples/performance/avoid-user-interaction-in-transactions/bad.al`.
|
||||
|
||||
29
microsoft/knowledge/performance/filter-before-find.md
Normal file
29
microsoft/knowledge/performance/filter-before-find.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [filter, setrange, setfilter, findset, scan]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Filter before you find
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Every call to FindSet, Find, or FindFirst on an unfiltered record variable scans the entire table. On hot tables (ledger entries, value entries, sales invoice lines) a production dataset can easily be millions of rows, so the cost of forgetting a filter is orders of magnitude worse than the cost of applying one.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Apply SetRange or SetFilter to narrow the record set before calling FindSet or Find. The filters should match a key on the table (see set-current-key-to-match-filters). When iterating rows that belong to a parent record, set all key-field filters before the find call — never inside the repeat loop.
|
||||
|
||||
See sample: `samples/performance/filter-before-find/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling FindSet with no filters and then discarding rows inside the loop with an if-statement forces the platform to read every row of the table before your code even runs.
|
||||
|
||||
See sample: `samples/performance/filter-before-find/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [event, subscriber, publisher, extension]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Keep event subscribers lightweight
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Event subscribers run synchronously on the publisher's thread. If a subscriber does heavy work — a database query, a web service call, a layout render — every caller of the publisher pays that cost. Subscribers on hot events (OnAfterValidate on common fields, OnBeforeInsert on ledger-entry-like tables) can multiply a small per-call cost into a system-wide regression.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Keep subscribers small: guard early with inexpensive checks, defer heavy work to a task queue or a background session, and cache results across invocations when the data is stable.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling an external web service, running a report, or iterating a large table from inside an event subscriber on a hot publisher makes every operation on that publisher as slow as the heaviest subscriber.
|
||||
|
||||
See sample: `samples/performance/keep-event-subscribers-lightweight/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [transaction, lock, scope, contention]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Keep transaction scope short
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Every write operation runs inside a transaction that holds locks until the transaction ends. Long transactions increase blocking, deadlocks, and timeouts for other sessions. The same work split across narrower transactions typically completes faster under load because it holds locks for less time.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Perform data reads, calculations, and external integrations outside the transaction whenever possible. Enter the writing phase with all inputs computed, execute the minimum set of Insert, Modify, and Delete calls, and exit. If you have a long-running batch, split it into checkpoints at safe boundaries (see avoid-commit-inside-loops).
|
||||
|
||||
See sample: `samples/performance/keep-transaction-scope-short/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Opening a transaction, then performing external web-service calls, heavy report runs, or user-facing dialogs while the locks are held, suspends every other session that needs the same rows for as long as the external operation takes.
|
||||
|
||||
See sample: `samples/performance/keep-transaction-scope-short/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [findset, get, aa0175, wasted-fetch, read]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Only fetch records you use
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop rule AA0175 flags code that retrieves a record and then does not use it. Every Find, FindSet, FindFirst, FindLast, or Get has a cost: the platform reads rows from SQL, materializes them, and transports them to the AL runtime. A call whose result is never read is wasted work, and on hot tables that work is never free.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Retrieve a record only when you need one or more of its field values. When you only need to know whether at least one row matches a filter, use IsEmpty (see use-isempty-for-existence-checks). When you only need a subset of fields, use SetLoadFields (see use-setloadfields-for-partial-records).
|
||||
|
||||
See sample: `samples/performance/only-fetch-records-you-use/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling FindSet or Get and then ignoring the result, or using it only as a boolean existence test, performs the full fetch and throws the data away.
|
||||
|
||||
See sample: `samples/performance/only-fetch-records-you-use/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [recordref, fieldref, dynamic, reflection]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Prefer direct record access over RecordRef where possible
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
RecordRef and FieldRef are the platform's reflection API: they work across tables the compiler does not know at authoring time. That flexibility costs per-operation overhead — every field access goes through a lookup — and loses compile-time type checking. For operations where the table is known, a strongly-typed Record variable is simpler and faster.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use Record variables for code paths that target a known table. Reach for RecordRef and FieldRef only when the table is genuinely dynamic (generic export/import, field-agnostic utilities, cross-table integrations).
|
||||
|
||||
See sample: `samples/performance/prefer-direct-record-over-recordref/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Using RecordRef as a habit, even when the target table is hardcoded two lines earlier, costs performance and hides intent from reviewers.
|
||||
|
||||
See sample: `samples/performance/prefer-direct-record-over-recordref/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [get, findfirst, primary-key, lookup]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Prefer Get for primary-key lookups
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Get is a direct primary-key lookup: one index seek, one row, done. FindFirst with SetRange on the primary key fields reaches the same row through a more general code path and carries the overhead of filter setup and a broader optimizer decision.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When the complete primary key is known, call Get. Use FindFirst only for non-primary-key lookups or when the filter is a partial prefix of the key.
|
||||
|
||||
See sample: `samples/performance/prefer-get-for-primary-key-lookups/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Setting one SetRange per primary-key field and then calling FindFirst reproduces Get with more typing and slightly worse performance.
|
||||
|
||||
See sample: `samples/performance/prefer-get-for-primary-key-lookups/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [setcurrentkey, key, index, sort, filter]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Set the current key to match your filters
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
AL chooses a key for a Find call based on the current SetCurrentKey selection. When filters do not align with any key, the platform either scans or falls back to a less selective index. On tables with production-scale row counts, this is the difference between an index seek and a table scan.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call SetCurrentKey with the fields you filter and sort on, in the order they appear in a table key. If no suitable key exists, add one via a table extension rather than relying on an unsupported filter pattern.
|
||||
|
||||
See sample: `samples/performance/set-current-key-to-match-filters/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Setting many filters on fields that no key covers, and leaving the key selection to the platform's heuristics, produces non-deterministic performance that degrades as the table grows.
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [report, addloadfields, ondatapreitem, layout, partial-record]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use AddLoadFields in report dataitems
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Reports iterate a dataitem's record automatically; the developer does not control the Find call directly. AddLoadFields, called in OnPreDataItem, tells the platform which fields the layout and the dataitem triggers will read. Without it the report streams every field of every row — for a ledger-entry dataitem on a production tenant, that is the dominant cost of the report.
|
||||
|
||||
## Best Practice
|
||||
|
||||
In each dataitem's OnPreDataItem trigger, call AddLoadFields for every field used by the layout, by the dataitem's triggers, and by any code that runs in the row-level event hooks. If the layout uses a FlowField, also ensure CalcFields is called and that the underlying key is loaded (see add-sift-keys-for-flowfields).
|
||||
|
||||
See sample: `samples/performance/use-addloadfields-in-report-layouts/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Omitting AddLoadFields is the default for reports generated by the AL wizard. For a dataitem backed by a ledger-entry table, this silently turns the report into a full-column scan.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [calcsums, sift, sum, aggregate, totals]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use CalcSums to aggregate filtered sets
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
When the task is to compute a sum over a filtered set, CalcSums lets the platform push the aggregation down to SQL using SIFT indexes. Iterating rows in AL to accumulate a total transports every row's data to the runtime only to discard it after adding one field. On ledger-entry-scale tables this difference is dramatic. The same SIFT infrastructure backs Sum-style FlowFields; when the value you need is already declared as a FlowField, calling CalcSums on the underlying table with the correct filters produces the same aggregate.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Set the required filters on the record, then call CalcSums on the field you want aggregated. Ensure the table has a key whose SumIndexFields includes the summed field and whose key prefix matches the filters (see add-sift-keys-for-flowfields).
|
||||
|
||||
See sample: `samples/performance/use-calcsums-for-flowfield-totals/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Looping a filtered set with FindSet and adding a field to an accumulator on every iteration performs work in AL that SQL already knows how to do in one aggregate query.
|
||||
|
||||
See sample: `samples/performance/use-calcsums-for-flowfield-totals/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [findset, lock, locktable, readonly, update]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use FindSet in read-only mode by default
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
FindSet has two modes: FindSet() and FindSet(false) are read-only and take no write lock; FindSet(true) calls LockTable before fetching. Write locks are expensive and hold for the remainder of the transaction, so passing `true` when you do not intend to modify the records increases contention under load.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call FindSet with no arguments when the loop only reads field values. Pass `true` only when the same loop is expected to call Modify, Delete, or Rename on the record, and the correctness of the operation depends on the table being locked for the full iteration.
|
||||
|
||||
See sample: `samples/performance/use-findset-readonly-by-default/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Writing FindSet(true) reflexively for every iteration forces the platform to take a LockTable on every call, even when the loop only reads values. The older two-parameter signature `FindSet(ForUpdate, UpdateKey)` is obsolete and must not be used.
|
||||
|
||||
See sample: `samples/performance/use-findset-readonly-by-default/bad.al`.
|
||||
|
||||
29
microsoft/knowledge/performance/use-findset-with-next.md
Normal file
29
microsoft/knowledge/performance/use-findset-with-next.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [findset, next, repeat, iteration, aa0181]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use FindSet with Next for iteration
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
When iterating over a filtered set of records with repeat-until, use FindSet together with Next. CodeCop rule AA0181 requires FindSet or Find to be paired with Next; using FindFirst or FindLast as the loop starter misrepresents intent and leads to rule AA0233.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call FindSet to start the iteration and Next to advance. Guard the loop with the standard `if FindSet() then ... until Next() = 0` idiom so callers can still handle the empty-set case.
|
||||
|
||||
See sample: `samples/performance/use-findset-with-next/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Starting a repeat-until loop with FindFirst or FindLast reads only one row and then calls Next on an iterator that was not intended for full-set traversal. The platform pays extra work to fetch the single row and the loop silhouette is misleading to reviewers.
|
||||
|
||||
See sample: `samples/performance/use-findset-with-next/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [insert, modify, delete, triggers, parameters]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Choose Insert, Modify, and Delete parameters deliberately
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Insert, Modify, and Delete accept a boolean that controls whether the table's OnInsert / OnModify / OnDelete trigger fires. Running the trigger for scratch or migrated data is often unnecessary work — side effects, posting rules, validations — for rows that were already validated upstream. Running the trigger when application logic depends on it is non-negotiable.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call Insert(true), Modify(true), or Delete(true) when the table's trigger logic is part of the operation's semantics. Call Insert(false), Modify(false), or Delete(false) when the operation is bulk data movement or temporary-table manipulation and the trigger would duplicate work or fire invalid side effects.
|
||||
|
||||
See sample: `samples/performance/use-insert-false-when-skipping-triggers/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Blindly passing `true` everywhere pays for triggers on rows that do not need them. Blindly passing `false` silently skips validations that the table's author intended to be mandatory.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [isempty, count, findfirst, existence]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use IsEmpty for existence checks
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
IsEmpty is the cheapest way to answer whether at least one row matches the current filters. It short-circuits at the first match and never hydrates a record. Count() scans and counts the entire set; FindFirst fetches a full row just to be discarded.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use `if not Rec.IsEmpty() then ...` for existence checks. Reserve Count for cases where the exact number of rows is needed, and FindFirst for cases where you actually want the row's field values.
|
||||
|
||||
See sample: `samples/performance/use-isempty-for-existence-checks/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`if Rec.Count() > 0` iterates the whole set just to answer a yes/no question. `if Rec.FindFirst() then` loads an entire row of data the caller never reads.
|
||||
|
||||
See sample: `samples/performance/use-isempty-for-existence-checks/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [setloadfields, partial-record, blob, bandwidth]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use SetLoadFields for partial records
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
SetLoadFields instructs the platform to hydrate only the listed fields on a record variable. On wide tables, or tables with BLOB or media fields, the difference is substantial: a Sales Invoice Line has dozens of fields and loading all of them for every row of a large set is wasted bandwidth. Primary key fields, SystemId, and system audit fields are always loaded automatically. SetLoadFields works only with FieldClass = Normal; FlowFields and FlowFilters cannot be partial-loaded.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call SetLoadFields before FindSet, FindFirst, or Get whenever the code path only reads a subset of fields. List every field that is read during the operation, including fields used in filters, calculations, and downstream function calls. Omitting a field that is later accessed triggers a second round-trip.
|
||||
|
||||
See sample: `samples/performance/use-setloadfields-for-partial-records/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Iterating a large set and reading only two or three fields without SetLoadFields forces the platform to transport every column for every row, including BLOBs and unused text fields.
|
||||
|
||||
See sample: `samples/performance/use-setloadfields-for-partial-records/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [singleinstance, cache, codeunit, session]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use SingleInstance codeunits for session caching
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
A SingleInstance codeunit lives once per session. Variables on it survive across calls, which makes it the natural home for data that is expensive to compute, read often, and stable for the duration of the session — feature flags, configuration snapshots, setup records. Each cached value avoids a SQL read per subsequent call site.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Store long-lived, read-often, rarely-changing data on a SingleInstance codeunit, populated lazily on first access. Keep the cached footprint small: a handful of booleans, a setup record, a few derived values. Be explicit about invalidation if the source can change during the session.
|
||||
|
||||
See sample: `samples/performance/use-single-instance-codeunits-for-caching/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Reading the same setup record on every call from every caller, instead of caching it, repeats a SQL round-trip that has no business happening more than once per session.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [strsubstno, string, concatenation, format]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use StrSubstNo for message formatting
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
StrSubstNo formats values into a placeholder template in a single call. Manual concatenation with `+` produces a chain of intermediate strings, each allocated and discarded, and mixes formatting rules inconsistently across locales. The performance difference per call is small; repeated inside a tight loop it is noticeable.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Declare the template as a Label (so it can be localized) and format with StrSubstNo. Pass values in the order the placeholders expect; StrSubstNo handles locale-sensitive conversions consistently.
|
||||
|
||||
See sample: `samples/performance/use-strsubstno-for-message-formatting/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Building a user-facing string by concatenating record field values with string literals ignores locale rules and allocates more than necessary.
|
||||
|
||||
See sample: `samples/performance/use-strsubstno-for-message-formatting/bad.al`.
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
bc-version: [26..28]
|
||||
domain: performance
|
||||
keywords: [temporary-table, in-memory, intermediate, working-set]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use temporary tables for intermediate data
|
||||
|
||||
> **Seed article.** Converted from an existing performance-review prompt to bootstrap the BCQuality performance corpus. Domain stewards should expand, restructure, and refine as needed.
|
||||
|
||||
## Description
|
||||
|
||||
Temporary tables live in memory, not in SQL. They are the correct primary data structure for intermediate results, working sets, and lookup caches that do not need to outlive the current operation. Using a real persisted table for scratch data incurs database round-trips, transaction scope, and locking for data that has no business being persisted.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Declare the record variable with `temporary` when the data is scratch. Populate it with Insert(false) to avoid firing triggers. Clear the table explicitly with DeleteAll when the variable's scope is long-lived (a SingleInstance codeunit or a reused session variable) and needs to be reset between uses.
|
||||
|
||||
See sample: `samples/performance/use-temporary-tables-for-intermediate-data/good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Writing intermediate results to a real table, processing them, and deleting them afterwards performs the full cost of INSERT and DELETE operations on data that never needed to be transactional.
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue