← Todos los artículos/ blog ·

Inserciones masivas en Business Central: cómo no romper el bulk insert

Business Central agrupa los Insert() y los envía a SQL Server de golpe. Qué lo vacía antes de tiempo, qué lo desactiva y cómo escribir bucles AL que lo aprovechen.

Tienes un proceso que inserta miles de movimientos: un registro de diario, una importación, una migración. Funciona, pero tarda mucho más de lo que debería. Muchas veces la causa no está en SQL Server, sino en una línea del bucle que, sin que lo sepas, obliga a Business Central a enviar cada Insert() por separado.

Qué es el bulk insert

Por defecto, el servidor de Business Central no ejecuta cada Insert() en el momento. Los guarda en un búfer y, cuando hace falta, los envía a SQL Server todos juntos.

Esto tiene dos ventajas:

  • Menos llamadas al servidor de base de datos. Mil inserciones agrupadas cuestan mucho menos que mil viajes de ida y vuelta.
  • Menos tiempo con las tablas bloqueadas. La inserción real se retrasa hasta el último momento posible de la transacción, así que los bloqueos duran menos. Se nota sobre todo en tablas con índices SIFT (los que alimentan los FlowField de sumas), que se actualizan con cada fila.

El bulk insert es automático: no hay que activarlo en el código. Lo que sí puede hacer el código es impedirlo sin querer.

Cuándo se envía el búfer a SQL Server

Los registros que esperan en el búfer se escriben en cuanto ocurre cualquiera de estas cosas:

  1. Se hace Commit() de la transacción.
  2. Se llama a Modify() o Delete() sobre la tabla.
  3. Se llama a cualquier método Find o Calc sobre la tabla: Find(), FindFirst(), FindLast(), FindSet(), Get(), CalcFields(), CalcSums()...

Ciclo del búfer: los Insert() se acumulan y se envían juntos al hacer Commit, Modify/Delete o Find/Calc sobre la misma tabla

La clave está en "sobre la tabla": leer de otra tabla no vacía el búfer. Si dentro del bucle lees la misma tabla en la que estás insertando, cada vuelta envía lo acumulado y el búfer nunca llega a llenarse. Tienes el coste de un insert por fila, como si el bulk insert no existiera.

Cuándo no se usa el búfer

Hay casos en los que Business Central ni siquiera intenta agrupar las inserciones:

Caso Por qué
El código usa el valor de retorno de Insert(), por ejemplo if GLEntry.Insert() then Para saber si ha ido bien, la fila tiene que escribirse en ese instante.
La tabla tiene campos Blob Estas tablas siempre se insertan fila a fila.
La tabla tiene campos con AutoIncrement = true Igual: no se agrupan.

El primero es el más traicionero, porque el patrón if not Rec.Insert() then Rec.Modify() es muy común para "insertar o actualizar". Es cómodo, pero en un bucle grande convierte cada fila en una llamada a SQL. Si sabes que el registro no existe, usa Rec.Insert() sin mirar el resultado; si no lo sabes, a menudo compensa comprobarlo antes en bloque.

El error típico: buscar el último número dentro del bucle

Este es el ejemplo que da Microsoft, adaptado a la sintaxis actual de AL. Se registran líneas de diario y cada movimiento necesita el siguiente "Entry No.":

// No aprovecha el bulk insert
GLEntry.LockTable();
if JnlLine.FindSet() then
    repeat
        GLEntry.FindLast();                         // Find sobre G/L Entry: vacía el búfer
        GLEntry.Init();
        GLEntry."Entry No." := GLEntry."Entry No." + 1;
        GLEntry."G/L Account No." := JnlLine."Account No.";
        GLEntry.Amount := JnlLine.Amount;
        GLEntry.Insert();
    until JnlLine.Next() = 0;
Commit();

El FindLast() está sobre G/L Entry, la misma tabla en la que se inserta. En cada vuelta obliga a escribir lo que había en el búfer, así que todas las inserciones van una a una.

La corrección es buscar el último número una sola vez, antes del bucle, y llevar la cuenta en una variable:

// Aprovecha el bulk insert
GLEntry.LockTable();
if GLEntry.FindLast() then
    NextEntryNo := GLEntry."Entry No." + 1
else
    NextEntryNo := 1;

if JnlLine.FindSet() then
    repeat
        GLEntry.Init();
        GLEntry."Entry No." := NextEntryNo;
        GLEntry."G/L Account No." := JnlLine."Account No.";
        GLEntry.Amount := JnlLine.Amount;
        GLEntry.Insert();
        NextEntryNo += 1;
    until JnlLine.Next() = 0;
Commit();                                           // aquí se hacen las inserciones

JnlLine.Next() sigue leyendo dentro del bucle, pero es otra tabla, así que no afecta al búfer de G/L Entry. Las inserciones se acumulan y se envían juntas en el Commit().

El LockTable() antes del FindLast() es importante: bloquea la tabla para que ningún otro proceso coja el mismo número entre la lectura y la inserción.

Cómo revisar tus propios bucles

Antes de dar por bueno un proceso que inserta muchas filas, repasa el cuerpo del bucle con estas preguntas:

  1. ¿Hay algún Get, Find*, CalcFields o CalcSums sobre la tabla destino? Sácalo fuera del bucle o guarda el dato en una variable.
  2. ¿Se hace Modify() o Delete() sobre la tabla destino entre inserciones? Si puedes, rellena el registro completo antes del Insert() en lugar de insertar y modificar después.
  3. ¿Se usa el resultado de Insert()? Si no lo necesitas, quítalo.
  4. ¿Hay un Commit() dentro del bucle? Además de vaciar el búfer en cada vuelta, rompe la atomicidad del proceso. Casi nunca es buena idea.
  5. ¿La tabla tiene Blob o AutoIncrement? Si es una tabla tuya y vas a insertar en volumen, valora si de verdad hacen falta. Para imágenes y ficheros, los tipos Media y MediaSet guardan el contenido en otra tabla del sistema.

Ojo también con los suscriptores de eventos. Si llamas a Insert(true), se ejecutan el trigger OnInsert y los eventos asociados; si alguna extensión lee o modifica la tabla desde ahí, el búfer se vacía aunque tu bucle esté limpio.

Desactivarlo para depurar: BufferedInsertEnabled

Hay un efecto secundario del búfer que conviene conocer: como la fila no se escribe en el Insert(), un error de inserción (una clave duplicada, por ejemplo) puede aparecer más tarde, en el Commit() o en el siguiente Find, y no en la línea que lo provoca. Eso despista mucho al depurar.

Para estos casos, en instalaciones on-premise se puede desactivar el bulk insert en el fichero CustomSettings.config del servidor de Business Central:

<add key="BufferedInsertEnabled" value="false" />

Hay que reiniciar la instancia del servicio para que se aplique. Con el búfer desactivado, el error aparece en el Insert() que lo causa. Una vez localizado el problema, vuelve a dejarlo como estaba: desactivarlo de forma permanente penaliza el rendimiento de todos los procesos.

En Business Central online no tienes acceso a la configuración del servidor, así que lo único que está en tu mano es escribir bien los bucles.

Resumen

  • Business Central agrupa los Insert() y los envía juntos a SQL Server: menos llamadas y bloqueos más cortos.
  • El búfer se vacía con Commit(), con Modify()/Delete() y con cualquier Find/Calc sobre la misma tabla.
  • No se agrupa si usas el resultado de Insert() o si la tabla tiene campos Blob o AutoIncrement.
  • Calcula los números de movimiento antes del bucle y no leas la tabla destino dentro de él.
  • Para depurar errores de inserción on-premise, BufferedInsertEnabled = false en CustomSettings.config, y solo mientras depuras.

Fuente: Bulk Inserts – Microsoft Learn.

¿Te encaja en tu proyecto?

Si quieres aplicar algo de esto en tu web o en tu Business Central, hablemos.