quarta-feira, 1 de agosto de 2012

Boas práticas para LOOP AT aninhados

Muitas vezes é necessário escrevermos códigos que leiam tabelas internas em sequência, sendo necessário diversos LOOPs. O comum é utilizarmos uma estrutura encadeada de LOOP AT dentro de LOOP AT. Como no exemplo abaixo:


REPORT zlooping.
DATA:
  lt_bkpf TYPE STANDARD TABLE OF bkpf,
  lt_bseg TYPE STANDARD TABLE OF bseg,
  ls_bkpf TYPE bkpf,
  ls_bseg TYPE bseg.
"Looping cabeçalho
LOOP AT lt_bkpf INTO ls_bkpf.
  "Looping item
  LOOP AT lt_bseg INTO ls_bseg WHERE bukrs = ls_bkpf-bukrs
                                                            AND belnr = ls_bkpf-belnr
                                                            AND gjahr = ls_bkpf-gjahr.
    "Faz alguma coisa
    "Faz outra coisa
  ENDLOOP.
ENDLOOP.


Há algo errado no código acima? Não absolutamente nada de errado. Ele irá fazer exatamente o que queremos fazer. Looping na BKPF seguido de looping na BSEG.
Agora digamos que na BKPF tem 10 registros e na BSEG 100. Provavelmente nosso looping seria rápido o bastante para não notarmos qualquer degradação do desempenho. Porém, imaginamos agora que nossa BKPF tem 100 registros e nossa BSEG 1000. Muito provavelmente o programa irá demorar um bocado para ser executado, pois serão muitas iterações. O programa lerá a BKPF 100 vezes e para cada iteração na BKPF a BSEG será lida 1000 vezes! Exatamente, a cada laço o programa sempre lerá TODA a BSEG. Neste caso, 100.000 iterações na BSEG.
Mas como podemos resolver esta questão de desempenho? De que forma podemos fazer uma sequência de loopings sem usar o LOOP AT + WHERE?
Abaixo segue uma forma de fazer com que seus loopings não fiquem tão demorados:


REPORT zlooping.
DATA:
  lt_bkpf TYPE STANDARD TABLE OF bkpf,
  lt_bseg TYPE STANDARD TABLE OF bseg,
  ls_bkpf TYPE bkpf,
  ls_bseg TYPE bseg.
"Ordenar para BINARY SEARCH
SORT:
  lt_bkpf BY bukrs belnr gjahr,
  lt_bseg BY bukrs belnr gjahr buzei.
"Looping cabeçalho
LOOP AT lt_bkpf INTO ls_bkpf.
  "Pega índice do registro (sy-tabix)
  READ TABLE lt_bseg
             WITH KEY bukrs = ls_bkpf-bukrs
                                  belnr = ls_bkpf-belnr
                                  gjahr = ls_bkpf-gjahr
             TRANSPORTING NO FIELDS
             BINARY SEARCH.
  "Verifica se encontrou registro
  CHECK sy-subrc = 0.
  "Looping item
  LOOP AT lt_bseg INTO ls_bseg FROM sy-tabix.
    "Verifica se chave do registro mudou, se sim, sai do looping no item
    IF ls_bkpf-bukrs <> ls_bseg-bukrs
       OR ls_bkpf-belnr <> ls_bseg-belnr
       OR ls_bkpf-gjahr <> ls_bseg-gjahr.
      EXIT.
    ENDIF.
    "Faz alguma coisa
    "Faz outra coisa
  ENDLOOP.
ENDLOOP.


Basicamente a mudança se dá na utilização de índices para leitura da tabela interna. 
O grande segredo aqui é a utilização do SORT e do READ TABLE com BINARY SEARCH. Utilizar BINARY SEARCH significa que a leitura na tabela interna não será sequêncial, ou seja, não irá ler uma linha de cada vez até terminar de ler todos os registros, a leitura será binária (leia sobre os riscos: http://agilpioneiro.wordpress.com/2012/04/02/binary-search-uma-roleta-russa-com-o-tambor-cheio).
Resumindo, não precisamos e não vamos ler a BSEG inteira, iremos ler a partir do primeiro registro encontrado para a chave informada até a chave do item mudar, quando não for mais a mesma chave do cabeçalho saí do looping no item e vai para o próximo registro do cabeçalho. Simples não?

E, para ficar melhor ainda, ao invés de utilizar o LOOP AT tabela INTO workarea, utilize LOOP AT tabela ASSIGNING fieldsymbol. Aqui teremos um ganho de desempenho, pois ao invés de copiar todo o registro para uma workarea iremos utilizar um ponteiro que indica a posição da linha da tabela na memória. Sempre que possível utilize ponteiros (FIELD-SYMBOL) para ler suas tabelas internas. Sempre haverá ganho de desempenho.


*&---------------------------------------------------------------------*
*& Report ZNESTED_LOOPING
*&
*&---------------------------------------------------------------------*
*& ABAP Sample Code
*& Performance in nested looping
*&
*& Maurício Lauffer - Brazil - 08.01.2012
*& http://www.linkedin.com/in/mauriciolauffer
*&
*& This sample explains how to use nested looping with performance
*&
*&---------------------------------------------------------------------*
REPORT znested_looping.
DATA:
  gt_bkpf TYPE STANDARD TABLE OF bkpf,
  gt_bseg TYPE STANDARD TABLE OF bseg.
FIELD-SYMBOLS:
   TYPE bkpf,
   TYPE bseg.
"Select header
SELECT * UP TO 500 ROWS
  FROM bkpf
  INTO TABLE gt_bkpf.
IF sy-subrc <> 0.
  RETURN.
ENDIF.
"Select items
SELECT *
  FROM bseg
  INTO TABLE gt_bseg
  FOR ALL ENTRIES IN gt_bkpf
  WHERE bukrs = gt_bkpf-bukrs
    AND belnr = gt_bkpf-belnr
    AND gjahr = gt_bkpf-gjahr.
"Looping into header
LOOP AT gt_bkpf ASSIGNING .
  "Get index (sy-tabix) for the first occurrence
  "You must guarantee that the table is sorted by the keys used in BINARY SEARCH
  READ TABLE gt_bseg TRANSPORTING NO FIELDS
       WITH KEY bukrs = -bukrs
                belnr = -belnr
                gjahr = -gjahr
       BINARY SEARCH.
  CHECK sy-subrc = 0.
  "Looping into items from index which we got before
  LOOP AT gt_bseg ASSIGNING FROM sy-tabix.
    "Check the table key, if it has changed, leave this looping
    IF -bukrs <> -bukrs
       OR -belnr <> -belnr
       OR -gjahr <> -gjahr.
      EXIT.
    ENDIF.
    "Do stuff...
  ENDLOOP.
ENDLOOP.




Link do código no GitHub:
https://github.com/mauriciolauffer/ABAP/blob/master/Performance/nested_looping.abap

3 comentários:

peixotmarc disse...

Muito bom post, principalmente a parte de desempenho sobre ponteiros que eu realmente desconhecia. Obrigado também pela menção ao nosso blog.

Marcel

mpeixoto disse...

Parabéns pelo post, desconhecia a utilização de ponteiros e as vantagens disso. Obrigado pela referência ao nosso blog www.agilpioneiro.wordpress.com

Anônimo disse...

Fantástico sempre achei que loop where funcionasse bem por ser algo proposto pelo standard.

Parabéns.