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
SELECTentre unLOOPy suENDLOOP, y tambiénSELECT ... ENDSELECTanidados, 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 INITIALes 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.
ASSIGNINGen lugar deINTO+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ón | Opción |
|---|---|
| Los datos del bucle vienen de un fichero, una BAPI o una pantalla | FOR ALL ENTRIES |
| Los datos del bucle vienen de otra SELECT | JOIN |
| El cruce se repite en varios desarrollos o el destino es S/4HANA / ABAP Cloud | Vista CDS |
| Son tres vueltas de bucle y siempre lo serán | Dé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".