El SELECT dentro del LOOP: cómo encontrarlo y tres formas de quitarlo

Si tuviera que elegir un único problema de rendimiento para buscar en un sistema SAP con años de desarrollos Z, sería este. Es fácil de escribir, funciona perfectamente en desarrollo con diez registros y se vuelve un problema serio en productivo con cincuenta mil.

LOOP AT lt_items INTO DATA(ls_item).
  SELECT SINGLE maktx FROM makt
    WHERE matnr = @ls_item-matnr
      AND spras = @sy-langu
    INTO @ls_item-maktx.
  MODIFY lt_items FROM ls_item.
ENDLOOP.

Por qué es tan caro

Cada SELECT es un viaje de ida y vuelta entre el servidor de aplicación y la base de datos. Aunque la consulta en sí tarde muy poco (MAKT tiene índice por material e idioma), el coste fijo de cada viaje se multiplica por el número de vueltas del bucle. Con 20.000 posiciones son 20.000 consultas para obtener datos que la base de datos podría devolver en una sola.

En HANA el efecto es todavía mayor: la base de datos es muy rápida procesando conjuntos grandes y comparativamente lenta atendiendo muchas peticiones pequeñas. Es justo lo contrario de lo que hace este código.

Cómo encontrarlo

  • SQL trace (ST05). Activas el trace, ejecutas el programa y en el resumen de sentencias idénticas aparece la misma SELECT ejecutada miles de veces.
  • ATC / Code Inspector. La comprobación de rendimiento "SELECT en bucles" lo detecta de forma estática, también cuando la SELECT está en un método llamado desde el bucle (aunque no siempre).
  • Lectura del código. Busca SELECT entre un LOOP y su ENDLOOP, y también SELECT ... ENDSELECT anidados, que son lo mismo con otra forma. CleanCore Check lo marca como error con la regla PERF-001.

Opción 1: FOR ALL ENTRIES

Es la sustitución más directa y funciona en cualquier versión. Se leen todos los textos de una vez y luego se buscan en memoria:

TYPES: BEGIN OF ty_text,
         matnr TYPE matnr,
         maktx TYPE maktx,
       END OF ty_text.
DATA lt_texts TYPE HASHED TABLE OF ty_text WITH UNIQUE KEY matnr.

IF lt_items IS NOT INITIAL.
  SELECT FROM makt
    FIELDS matnr, maktx
    FOR ALL ENTRIES IN @lt_items
    WHERE matnr = @lt_items-matnr
      AND spras = @sy-langu
    INTO TABLE @lt_texts.
ENDIF.

LOOP AT lt_items ASSIGNING FIELD-SYMBOL(<ls_item>).
  <ls_item>-maktx = VALUE #( lt_texts[ matnr = <ls_item>-matnr ]-maktx OPTIONAL ).
ENDLOOP.

Tres detalles que marcan la diferencia:

  • El IF lt_items IS NOT INITIAL es obligatorio. Si la tabla está vacía, FOR ALL ENTRIES ignora la condición y lee la tabla entera. Es de los errores más caros que se pueden ver en productivo.
  • La tabla de destino es HASHED. Buscar en una tabla STANDARD dentro del bucle es otra vez coste cuadrático, solo que en memoria. Con HASHED cada búsqueda es inmediata.
  • ASSIGNING en lugar de INTO + MODIFY. Se modifica la línea directamente, sin copiarla dos veces.

FOR ALL ENTRIES elimina duplicados del resultado y el kernel lo divide en bloques, así que no es gratis: con tablas de entrada enormes conviene quitar duplicados antes (SORT + DELETE ADJACENT DUPLICATES).

Opción 2: un JOIN

Si los datos del bucle salen a su vez de una SELECT, lo más eficiente es que la base de datos haga el cruce y devuelva el resultado ya montado:

SELECT FROM ekpo AS p
  LEFT OUTER JOIN makt AS t
    ON  t~matnr = p~matnr
    AND t~spras = @sy-langu
  FIELDS p~ebeln, p~ebelp, p~matnr, t~maktx, p~menge
  WHERE p~ebeln IN @s_ebeln
  INTO TABLE @DATA(lt_items).

El LEFT OUTER JOIN es importante: con un INNER JOIN, las posiciones sin texto en el idioma de conexión desaparecerían del resultado, que no es lo que hacía el código original.

Opción 3: una vista CDS

Cuando el mismo cruce se usa en varios programas, o el desarrollo va a S/4HANA, lo natural es llevarlo a una vista CDS. En ABAP Cloud, además, se construye sobre vistas liberadas en lugar de sobre tablas:

define view entity ZI_PurchaseOrderItemText
  as select from I_PurchaseOrderItemAPI01 as Item
  association [0..1] to I_ProductDescription as _Text
    on  _Text.Product  = $projection.Material
    and _Text.Language = $session.system_language
{
  key Item.PurchaseOrder,
  key Item.PurchaseOrderItem,
      Item.Material,
      _Text.ProductDescription,
      Item.OrderQuantity,
      Item.PurchaseOrderQuantityUnit
}

El programa queda en una SELECT de una línea y la lógica del cruce está en un único sitio, reutilizable desde ABAP, desde un servicio OData o desde una app analítica.

¿Cuál elegir?

SituaciónOpción
Los datos del bucle vienen de un fichero, una BAPI o una pantallaFOR ALL ENTRIES
Los datos del bucle vienen de otra SELECTJOIN
El cruce se repite en varios desarrollos o el destino es S/4HANA / ABAP CloudVista CDS
Son tres vueltas de bucle y siempre lo seránDéjalo, pero coméntalo

Cuándo no merece la pena

Si el bucle recorre una tabla que por definición tiene pocas líneas (las sociedades de un grupo, los centros de un país) y la tabla leída está en buffer, la ganancia es despreciable y el código original se lee mejor. La regla no es "nunca un SELECT en un bucle", sino "nunca sin saber cuántas vueltas va a dar en productivo".

Si tienes un report con este problema y quieres una segunda opinión, pásalo por CleanCore Check. Te marca cada SELECT en bucle con su línea.