In this video, I talk about the different Rec.Find functions and how to s figure out the right one to use.

If you want to learn more about how to work with records, check out my video on validation.
In this video, Erik walks through one of the most fundamental topics in Business Central AL development: how to correctly use the various Find functions when working with records. He covers the history behind these functions, explains when to use each one, and highlights common mistakes that can silently hurt performance without breaking functionality.
The Origins of the Find Command
To understand why there are multiple Find functions in AL, it helps to know a bit of history. When Navision was first created, SQL Server wasn’t really a thing yet, so the developers built their own proprietary database — the native database. This database was cursor-based by design, meaning the system always knew the current record’s position in a table and could move forward or backward from there.
The original Find command was built for this cursor-based model and accepted a parameter to control how records were located:
'-'— Find the first record'+'— Find the last record'='— Find an exact match on the primary key (equivalent toGet)'>='— Equal or next'<='— Equal or previous'>'— Next record'<'— Previous record
The Next function takes a steps parameter. If you don’t pass one, it defaults to 1. It returns the number of steps it was actually able to take — so if you ask for 2 steps forward but only one record exists, it returns 1. When it returns 0, there are no more records to move to.
The classic loop pattern looked like this:
if Customer.Find('-') then
repeat
// do something
until Customer.Next() = 0;
The Problem with SQL Server
When SQL Server support was introduced (around Navision Financials 2.5/2.6), a problem emerged: SQL Server is slow with cursor-based access. The entire AL language had been designed around cursors for the native database, so in most real-time usage scenarios, SQL Server was actually slower than the native database.
To solve this, Microsoft introduced three new Find methods that were optimized for set-based SQL operations. This is where much of the confusion around Find functions begins.
FindFirst and FindLast
FindFirst and FindLast do exactly what their names suggest — they find the first or last record matching the current filters. However, they come with an important rule:
Do not use FindFirst or FindLast if you plan to loop through records afterward. These functions should only be used when you genuinely need just the first or last record and nothing else.
// Correct usage: you only need the first matching record
if Customer.FindFirst() then
// do something with this single record
IsEmpty: The Hidden “Find”
There’s another function that doesn’t have “Find” in its name but serves a similar purpose. Consider this pattern:
if Customer.FindFirst() then
DoSomethingImportant();
If you’re not actually using the record data and you just want to know whether any matching records exist, there’s a much better option:
Customer.SetFilter(Name, 'Erik*');
if not Customer.IsEmpty() then
DoSomethingImportant();
IsEmpty is faster because it doesn’t need to actually retrieve the record — it only checks for existence. If you’re not going to use the record’s fields, always prefer IsEmpty over FindFirst.
FindSet: The Right Choice for Loops
FindSet is the function you should always use when you plan to loop through a set of records with repeat...until Next() = 0. It tells the server tier to load records in bulk, making iteration significantly faster than the old Find('-') approach.
FindSet takes two optional Boolean parameters:
- ForUpdate — Set to
trueif you intend to modify the records in the loop. This immediately acquires a lock on the table before reading begins. - UpdateKey — Set to
true(in addition to the first parameter beingtrue) if you know you’ll be modifying key field values. Note thatfalse, truedoesn’t make sense — the second parameter is always an addition to the first.
// Read-only loop — no modifications
if Customer.FindSet(false, false) then
repeat
// read-only operations
until Customer.Next() = 0;
// Loop with modifications to non-key fields
if Customer.FindSet(true, false) then
repeat
// modify non-key fields
Customer.Modify();
until Customer.Next() = 0;
// Loop with modifications to key fields
if Customer.FindSet(true, true) then
repeat
// modify key fields
Customer.Modify();
until Customer.Next() = 0;
Erik mentions that he usually writes FindSet(false, false) explicitly in code, even though both parameters default to false, because it makes the intent immediately visible — you can see at a glance that the loop is read-only.
One Limitation of FindSet
FindSet always iterates from the first record to the last. If you need to loop backwards (from last to first), you still need to use the old Find('+') with Next(-1):
if Customer.Find('+') then
repeat
// processing in reverse order
until Customer.Next(-1) = 0;
Optimizing with Filters and Keys
Before calling FindSet, you should set up your filters and, when beneficial, set the current key to help SQL Server execute the query efficiently. The Customer table, for example, has 16 keys defined. Keys serve two purposes:
- Sorting — If there’s already a key matching your desired sort order, SQL Server doesn’t have to do extra work.
- Filtering — The back end will try to provide SQL query hints based on keys. If you’re filtering on a field that has a key, setting that key explicitly will use fewer server resources and improve performance.
Here’s the complete recommended pattern from Erik’s source code:
codeunit 50134 "Find The Right Way"
{
procedure Test()
var
Customer: Record Customer;
begin
Customer.SetFilter(Name, 'Erik*');
Customer.SetCurrentKey(Name);
if Customer.FindSet() then
repeat
// process records
until Customer.Next() = 0;
end;
}
The Dangerous Part: Nothing Breaks
Perhaps the most important takeaway from this video is that using the wrong Find function won’t break your code. All of these approaches will work correctly:
// Works, but slow (cursor-based)
if Customer.Find('-') then
repeat
until Customer.Next() = 0;
// Works, but not optimized for looping
if Customer.FindFirst() then
repeat
until Customer.Next() = 0;
// Best approach for looping
if Customer.FindSet() then
repeat
until Customer.Next() = 0;
The danger is that you won’t see an error or a warning — your code will simply run slower than it needs to. This makes it a silent performance issue that can accumulate across an entire application.
Quick Reference Summary
FindSet()— Use when looping through records. Always the right choice forrepeat...until Next() = 0patterns.FindFirst()/FindLast()— Use only when you need a single record (the first or last). Do not loop after these.IsEmpty()— Use when you only need to check whether records exist, without needing the actual data.Find('+') / Find('-')— Legacy syntax. Still needed for reverse iteration (Find('+')withNext(-1)), but otherwise prefer the newer functions.Get()— Use when you know the exact primary key value. Equivalent toFind('=').SetCurrentKey()— Set beforeFindSetwhen you’re filtering or sorting on fields that have a defined key, to help SQL Server optimize the query.
Understanding these distinctions is one of those foundational skills in AL development that separates performant code from sluggish code. The functions all work, but choosing the right one ensures your extensions scale well as data volumes grow.