Technical6 min read

insert_recordset, update_recordset, delete_from: X++ performance

X++ set-based operations process thousands of rows in one SQL round trip. Syntax, Learn examples and the cases where they fall back to row by row.

The short answer

In X++, insert_recordset, update_recordset and delete_from act on a set of records in a single database round trip, instead of a while select loop calling insert(), update() or delete() for each row. On large volumes, it is often the code change that brings the biggest performance gain.

But these statements automatically fall back to row-by-row processing in several cases: an overridden insert/update/delete method on the table, database log enabled, alerts set up, delete actions, non-SQL tables or valid-time-state tables. The skipDataMethods, skipDatabaseLog, skipEvents and skipDeleteActions methods let you ignore some of these factors, to be used knowingly.

insert_recordset: copy in one statement

insert_recordset copies data from one or more source tables into a target table in one round trip. The target field list must match, in order, the selected field list; missing fields get 0 or empty, and RecId is assigned by the kernel. Microsoft Learn example, aggregating by name:

ValueSumByName valueSumName;
NameValuePair  nameValuePair;

insert_recordset valueSumName (Name, ValueSum)
    select Name, sum(Value)
    from nameValuePair
    group by Name;

You can also join several tables and insert variable values (with firstonly for a single row). Literals such as 128 or "text" are not accepted as a source. If you need to transform each row before inserting, RecordInsertList or RecordSortedList (insertDatabase) also batch inserts into one call, with more flexibility but less speed than insert_recordset.

update_recordset and delete_from

update_recordset changes several rows in one statement, like an SQL UPDATE … SET, and accepts joins to feed the values. Learn example:

CustTable custTable;

ttsBegin;
update_recordset custTable
    setting CreditMax = custTable.CreditMax + 1000
    where custTable.CreditMax > 0;
ttsCommit;

delete_from removes every row matching the condition, with optional exists join or notexists join. The contrast with a loop:

// Avoid: one SQL call per row
ttsBegin;
while select forUpdate tabWidget
    where tabWidget.quantity <= 100
{
    tabWidget.delete();
}
ttsCommit;

// Prefer: a single statement
ttsBegin;
delete_from tabWidget
    where tabWidget.quantity <= 100;
ttsCommit;

When does the operation fall back to row by row?

According to Microsoft Learn, the conversion to row-by-row processing happens in these cases:

  • non-SQL tables (for example in-memory temporary tables): for all three statements;
  • delete actions: for delete_from only, bypassed with skipDeleteActions;
  • database log enabled on the table: bypassed with skipDatabaseLog;
  • overridden insert, update or delete method: bypassed with skipDataMethods;
  • alerts set up on the table: bypassed with skipEvents;
  • ValidTimeStateFieldType property other than None: no bypass.

Note: if the conversion happens anyway, the skip* settings are ignored. And skipping data methods also skips the business logic they contain; only do it if you know that logic does not need to run.

Common mistakes

  • Writing an update_recordset on a table whose update() is overridden and assuming the operation is set-based: it runs as a loop.
  • Adding skipDataMethods(true) to "speed up" without checking what the overridden method does: checks and related updates are skipped.
  • Forgetting ttsBegin / ttsCommit around updates and deletes.
  • Misaligning the field order between the target and the select of an insert_recordset.
  • Filling a report's temporary table with an insert() loop: an insert_recordset is often possible. See SSRS report with RDP and Contract classes.

Where to use it

Purge, recalculation or copy batch jobs, filling report temporary tables, data migrations: see SysOperation batch job step by step. In a code review, whether the code was written by a person or generated by AI, "can this be set-based?" is one of the basic checks. Skalp AI generates X++ code with unit tests, delivered to your Azure DevOps and validated by your IT lead: see AI X++ development, building X++ without a consultant in 2026 and the Basic, Pro and Max plans.

Sources

Frequently asked questions

insert_recordset or RecordInsertList?

insert_recordset is faster when the data comes straight from other tables. RecordInsertList is more flexible if you must prepare each row in X++ before inserting; it also batches the inserts into one call.

Why is my update_recordset slow?

It has probably fallen back to row by row: overridden update method, database log, alerts or a valid-time-state table. Check these before considering skipDataMethods, skipDatabaseLog or skipEvents.

Are skip* methods always honoured?

No. According to Microsoft Learn, if the operation is converted to row by row anyway, all skip* settings are ignored.

Ready to transform your D365?

Join the CIOs who develop their D365 tickets in 1h for 1 credit (≈ €400 on Basic, €200 on Pro).