This comprehensive guide outlines best practices and advanced techniques for optimizing AL code performance in Microsoft Business Central applications.
// Example 1: Basic SetLoadFields usage for customer processing
procedure ProcessCustomersEfficiently()
var
Customer: Record Customer;
ProcessedCount: Integer;
begin
// Load only the fields needed for processing
Customer.SetLoadFields("No.", "Name", "Credit Limit (LCY)", "Balance (LCY)");
Customer.SetRange(Blocked, Customer.Blocked::" ");
if Customer.FindSet() then
repeat
// Process only loaded fields for optimal performance
if Customer."Balance (LCY)" > Customer."Credit Limit (LCY)" then
ProcessOverLimitCustomer(Customer."No.", Customer.Name);
ProcessedCount += 1;
until Customer.Next() = 0;
Message('Processed %1 customers efficiently', ProcessedCount);
end;
// Example 2: Advanced SetLoadFields with calculated fields
procedure GetCustomerStatistics(var TempCustomerStats: Record "Customer Statistics" temporary)
var
Customer: Record Customer;
CustomerLedgerEntry: Record "Cust. Ledger Entry";
begin
// Efficient customer data loading
Customer.SetLoadFields("No.", "Name", "Customer Posting Group");
if Customer.FindSet() then
repeat
TempCustomerStats.Init();
TempCustomerStats."Customer No." := Customer."No.";
TempCustomerStats."Customer Name" := Customer.Name;
// Efficient ledger entry processing with SetLoadFields
CustomerLedgerEntry.SetLoadFields("Remaining Amount", "Amount");
CustomerLedgerEntry.SetRange("Customer No.", Customer."No.");
CustomerLedgerEntry.SetRange(Open, true);
CustomerLedgerEntry.CalcSums("Remaining Amount");
TempCustomerStats."Outstanding Amount" := CustomerLedgerEntry."Remaining Amount";
TempCustomerStats.Insert();
until Customer.Next() = 0;
end;
// Example 3: SetLoadFields in complex scenarios with related tables
procedure AnalyzeCustomerPurchasePatterns()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
CustomerAnalysis: Record "Customer Analysis" temporary;
TotalAmount: Decimal;
begin
// Load only essential customer fields
Customer.SetLoadFields("No.", "Name", "ABC Customer Category");
Customer.SetRange("ABC Customer Category", Customer."ABC Customer Category"::Premium);
if Customer.FindSet() then
repeat
Clear(CustomerAnalysis);
CustomerAnalysis."Customer No." := Customer."No.";
CustomerAnalysis."Customer Name" := Customer.Name;
// Efficient sales analysis with minimal field loading
SalesHeader.SetLoadFields("No.", "Sell-to Customer No.", "Document Date");
SalesHeader.SetRange("Sell-to Customer No.", Customer."No.");
SalesHeader.SetRange("Document Date", CalcDate('<-1Y>', Today), Today);
if SalesHeader.FindSet() then
repeat
SalesLine.SetLoadFields("Document No.", "Line Amount");
SalesLine.SetRange("Document No.", SalesHeader."No.");
SalesLine.CalcSums("Line Amount");
TotalAmount += SalesLine."Line Amount";
until SalesHeader.Next() = 0;
CustomerAnalysis."Total Purchase Amount" := TotalAmount;
CustomerAnalysis.Insert();
TotalAmount := 0;
until Customer.Next() = 0;
end;- Use appropriate filters before reading records
- Use SetRange/SetFilter with indexed fields when possible
- Apply filters in the order of selectivity (most selective first)
- Avoid using FIND('-') without filters
// Good: Efficient filtering with indexed fields
Customer.SetRange("Customer Posting Group", PostingGroup);
Customer.SetRange("Blocked", Customer.Blocked::" ");
Customer.SetLoadFields("No.", "Name");
// Avoid: Unfiltered searches
Customer.FindSet(); // This loads all customers without filters// BEFORE: Inefficient approach with nested database calls
procedure CalculateCustomerTotalsInefficient()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
TotalAmount: Decimal;
begin
if Customer.FindSet() then // Loads all fields for all customers
repeat
// Database call inside loop - AVOID
SalesHeader.SetRange("Sell-to Customer No.", Customer."No.");
if SalesHeader.FindSet() then
repeat
// Another nested database call - VERY INEFFICIENT
SalesLine.SetRange("Document No.", SalesHeader."No.");
SalesLine.CalcSums("Line Amount");
TotalAmount += SalesLine."Line Amount";
until SalesHeader.Next() = 0;
// Process customer with total...
until Customer.Next() = 0;
end;
// AFTER: Optimized approach with SetLoadFields and bulk operations
procedure CalculateCustomerTotalsOptimized()
var
Customer: Record Customer;
SalesLine: Record "Sales Line";
TempCustomerTotals: Record "Customer Totals" temporary;
CustomerNo: Code[20];
begin
// Step 1: Load only required customer fields
Customer.SetLoadFields("No.", "Name");
Customer.SetRange(Blocked, Customer.Blocked::" ");
// Step 2: Bulk load sales data with efficient field selection
SalesLine.SetLoadFields("Sell-to Customer No.", "Line Amount");
SalesLine.SetRange("Document Date", CalcDate('<-1Y>', Today), Today);
// Step 3: Process in memory using temporary tables
if SalesLine.FindSet() then
repeat
TempCustomerTotals.Reset();
TempCustomerTotals.SetRange("Customer No.", SalesLine."Sell-to Customer No.");
if TempCustomerTotals.FindFirst() then begin
TempCustomerTotals."Total Amount" += SalesLine."Line Amount";
TempCustomerTotals.Modify();
end else begin
TempCustomerTotals.Init();
TempCustomerTotals."Customer No." := SalesLine."Sell-to Customer No.";
TempCustomerTotals."Total Amount" := SalesLine."Line Amount";
TempCustomerTotals.Insert();
end;
until SalesLine.Next() = 0;
// Step 4: Final processing with minimal database operations
if Customer.FindSet() then
repeat
TempCustomerTotals.SetRange("Customer No.", Customer."No.");
if TempCustomerTotals.FindFirst() then
ProcessCustomerTotal(Customer."No.", Customer.Name, TempCustomerTotals."Total Amount");
until Customer.Next() = 0;
end;
// OPTIMIZED PATTERN: Bulk operations with efficient indexing
procedure BulkUpdateCustomerRatings()
var
Customer: Record Customer;
CustomerRating: Record "ABC Customer Rating";
TempRatingAverage: Record "Rating Average" temporary;
CustomerNo: Code[20];
TotalScore: Decimal;
RatingCount: Integer;
begin
// Step 1: Calculate averages in one pass
CustomerRating.SetLoadFields("Customer No.", "Rating Score");
if CustomerRating.FindSet() then
repeat
TempRatingAverage.SetRange("Customer No.", CustomerRating."Customer No.");
if TempRatingAverage.FindFirst() then begin
TempRatingAverage."Total Score" += CustomerRating."Rating Score";
TempRatingAverage."Rating Count" += 1;
TempRatingAverage."Average Score" := TempRatingAverage."Total Score" / TempRatingAverage."Rating Count";
TempRatingAverage.Modify();
end else begin
TempRatingAverage.Init();
TempRatingAverage."Customer No." := CustomerRating."Customer No.");
TempRatingAverage."Total Score" := CustomerRating."Rating Score";
TempRatingAverage."Rating Count" := 1;
TempRatingAverage."Average Score" := CustomerRating."Rating Score";
TempRatingAverage.Insert();
end;
until CustomerRating.Next() = 0;
// Step 2: Bulk update customers
Customer.SetLoadFields("No.", "ABC Average Rating");
if TempRatingAverage.FindSet() then
repeat
if Customer.Get(TempRatingAverage."Customer No.") then begin
Customer."ABC Average Rating" := TempRatingAverage."Average Score";
Customer.Modify();
end;
until TempRatingAverage.Next() = 0;
end;// Example 1: Using Query object for complex data aggregation
query 50100 "Customer Rating Analysis"
{
QueryType = Normal;
elements
{
dataitem(Customer; Customer)
{
filter(CustomerNoFilter; "No.")
{ }
filter(CategoryFilter; "ABC Customer Category")
{ }
column(CustomerNo; "No.")
{ }
column(CustomerName; Name)
{ }
column(CustomerCategory; "ABC Customer Category")
{ }
dataitem(CustomerRating; "ABC Customer Rating")
{
DataItemLink = "Customer No." = Customer."No.";
SqlJoinType = LeftOuterJoin;
column(AvgRatingScore; "Rating Score")
{
Method = Average;
}
column(TotalRatings; "Rating Score")
{
Method = Count;
}
column(MinRatingScore; "Rating Score")
{
Method = Min;
}
column(MaxRatingScore; "Rating Score")
{
Method = Max;
}
}
}
}
}
// Using the query for efficient data retrieval
procedure GetCustomerRatingAnalysis()
var
CustomerRatingAnalysis: Query "Customer Rating Analysis";
TempAnalysisResult: Record "Analysis Result" temporary;
begin
CustomerRatingAnalysis.SetFilter(CategoryFilter, '%1|%2',
Customer."ABC Customer Category"::Premium,
Customer."ABC Customer Category"::Standard);
CustomerRatingAnalysis.Open();
while CustomerRatingAnalysis.Read() do begin
TempAnalysisResult.Init();
TempAnalysisResult."Customer No." := CustomerRatingAnalysis.CustomerNo;
TempAnalysisResult."Customer Name" := CustomerRatingAnalysis.CustomerName;
TempAnalysisResult."Average Rating" := CustomerRatingAnalysis.AvgRatingScore;
TempAnalysisResult."Total Ratings" := CustomerRatingAnalysis.TotalRatings;
TempAnalysisResult.Insert();
end;
CustomerRatingAnalysis.Close();
// Process results efficiently in memory
ProcessAnalysisResults(TempAnalysisResult);
end;
// Example 2: Efficient data loading with proper indexing
procedure OptimizedCustomerSearch(SearchText: Text; var TempCustomer: Record Customer temporary)
var
Customer: Record Customer;
begin
// Use proper filtering on indexed fields
Customer.SetLoadFields("No.", "Name", "Phone No.", "E-Mail");
// Apply filters in order of selectivity (most selective first)
if StrLen(SearchText) > 0 then begin
Customer.SetFilter("No.", '@*' + SearchText + '*');
Customer.SetFilter(Name, '@*' + SearchText + '*');
end;
// Limit results for performance
Customer.SetRange(Blocked, Customer.Blocked::" ");
if Customer.FindSet() then begin
if Customer.Count > 1000 then
Message('Search returned more than 1000 results. Consider refining your search.');
repeat
TempCustomer.TransferFields(Customer);
TempCustomer.Insert();
until (Customer.Next() = 0) or (TempCustomer.Count >= 1000);
end;
end;- Declare record variables as temporary for in-memory operations
- Process data in memory before writing to database
- Use temporary tables for sorting and filtering operations
// Efficient temporary table usage
procedure ProcessCustomerData()
var
TempCustomer: Record Customer temporary;
begin
LoadCustomersToTemp(TempCustomer);
TempCustomer.ProcessInMemory();
TempCustomer.WriteResultsToDatabase();
end;- Keep transactions as short as possible
- Avoid user interaction during transactions
- Use LockTable() only when necessary and as late as possible
- Consider using snapshot isolation for read operations
// Efficient memory usage with proper variable scope
procedure ProcessLargeDataset()
var
TempBuffer: Record "Integer" temporary;
begin
// Use temporary records for large datasets
LoadDataToTempBuffer(TempBuffer);
ProcessTempBuffer(TempBuffer);
// TempBuffer automatically cleaned up when procedure exits
end;- Use appropriate data types for fields and variables
- Minimize the use of Text variables with large lengths
- Consider using BLOB fields for large text content
- Implement proper cleanup for temporary objects
- Move complex calculations to separate procedures
- Use CurrPage.UPDATE(FALSE) to avoid unnecessary refreshes
- Consider using background tasks for heavy calculations
// Optimized OnAfterGetRecord
trigger OnAfterGetRecord()
begin
// Minimal processing in trigger
CalculatedField := GetCachedCalculation(Rec."No.");
end;
// Heavy processing moved to separate procedure
procedure RefreshCalculations()
begin
// Complex calculations performed on demand
PerformComplexCalculations();
CurrPage.Update();
end;- Avoid excessive CALCFIELDS calls, especially in loops
- Use SetAutoCalcFields only for fields that are always needed
- Consider using normal fields with manual updates for frequently accessed calculated values
- Use DisableControls/EnableControls when updating multiple records
- Implement virtual scrolling for large datasets
- Minimize the number of visible fields on list pages
- Use page extensions instead of replacing entire pages
// Optimize page loading with SetLoadFields
trigger OnOpenPage()
begin
SetLoadFields("No.", "Name", "Status"); // Load only essential fields
if FindSet() then; // Prepare recordset efficiently
end;- Use StartSession for non-interactive processing
- Consider job queue entries for scheduled operations
- Implement proper progress reporting for long-running tasks
// Background processing pattern
procedure ProcessLargeDatasetInBackground()
var
JobQueueEntry: Record "Job Queue Entry";
begin
JobQueueEntry.ScheduleJobQueueEntry(
Codeunit::"Large Dataset Processor",
Rec.RecordId);
end;// Performance measurement pattern
procedure MeasurePerformance()
var
StartTime: DateTime;
Duration: Duration;
begin
StartTime := CurrentDateTime;
// Performance-critical operation
PerformCriticalOperation();
Duration := CurrentDateTime - StartTime;
LogPerformanceMetric('CriticalOperation', Duration);
end;- Implement telemetry to track operation durations
- Use the performance profiler to identify bottlenecks
- Set up alerts for slow-running operations
- Regularly review performance metrics with Business Central telemetry
- Use appropriate filters to limit data retrieval
- Consider using processing-only reports for data manipulation
- Use temporary tables to prepare data before rendering
// Efficient report data preparation
trigger OnPreDataItem()
begin
SetLoadFields("No.", "Name"); // Optimize field loading
SetFilter("Date Filter", GetDateFilter()); // Apply business logic filters
end;- Use indexed fields in filters and sorting
- Avoid complex calculations in WHERE clauses
- Use EXISTS/IN instead of joins when appropriate
- Monitor and optimize slow-running queries through telemetry
// Efficient query usage for complex operations
query CustomerSalesAnalysis
{
elements
{
dataitem(Customer; Customer)
{
filter(PostingGroup; "Customer Posting Group")
{
// Indexed field for efficient filtering
}
column(CustomerNo; "No.")
column(CustomerName; Name)
dataitem(SalesLine; "Sales Line")
{
DataItemLink = "Sell-to Customer No." = Customer."No.";
column(TotalAmount; Amount)
{
Method = Sum;
}
}
}
}
}- Cache lookup values that don't change frequently
- Use application cache for shared data
- Implement proper cache invalidation when data changes
// Caching pattern for lookup data
procedure GetCachedSetupValue(KeyValue: Code[20]): Text
var
TempSetup: Record "Setup Table" temporary;
begin
if not TempSetup.Get(KeyValue) then begin
TempSetup.LoadFromDatabase(KeyValue);
TempSetup.Insert();
end;
exit(TempSetup."Setup Value");
end;// Efficient bulk operations
procedure BulkUpdateCustomers()
var
Customer: Record Customer;
begin
Customer.SetLoadFields("Credit Limit (LCY)", "Last Modified Date Time");
if Customer.FindSet(true) then
repeat
Customer."Credit Limit (LCY)" := CalculateNewLimit(Customer);
Customer.Modify(false); // Skip validation for bulk operations
until Customer.Next() = 0;
end;- Implement automated performance tests
- Use telemetry data to identify regression
- Set performance benchmarks for critical operations
- Monitor resource utilization during peak usage