28 de abril de 2014

Loops encadeados: a maldição

Fala pessoas!

Olha, teve 1 acesso desde o último post. Será que alguém errou algum endereço e caiu aqui?

Bom, hoje vou falar de uma coisa simples e besta, mas que tenho visto muitas vezes essa coisa simples e besta se repetindo da forma mais dantesca o possível: Loop encadeado (ou vulgo loop dentro de loop).

Por conceito, o loop é um processo que navega por todos os registros de uma tabela interna. Simples não? Conceitualmente sim e praticamente também. Então qual o problema? O problema está na necessidade de analisar os dados que serão processados antes da construção do loop.

Vejam esses dois exemplos:

  1. um loop encadeado com 10 registros na primeira tabela e 20 na segunda tabela. A execução deste loop encadeado resultaria em 200 processos (ou 125 na base 13, certo?).

  2. um loop encadeado de gente grande (mas não tão), com 96.701 registros na primeira tabela e 1.504.265 na segunda. A execução deste belo loop encadeado resultaria em simples 145.463.929.765 processos. Imagine que legal fazer um loop encadeado da forma mais hedionda o possível e se deparar com uma quantidade de registros dessas. Será muito legal justificar que seu report vai demorar só 2 anos, 7 meses, 19 dias, 21 horas e 42 minutos para terminar.
Mas com certeza, tem alguém pensando que isso é puro exagero e que mesmo um loop encadeado mais tosco o possível não pode demorar tanto assim, que ele sempre fez assim e sempre funcionou. No exemplo abaixo eu fiz um teste de tempo de execução para o exemplo 2 (olha que legal, o primeiro tempo apresentado é o do loop tosco):



Então, vamos ver os tipos mais comuns de loops encadeados, do pior para o melhor:

1 - Loop preguiça - para pouco, realmente poucos dados
 Esse é o tipo de loop mais simples e que tem a pior performance possível. É somente um loop com outro interno usando um IF para comparação dos dados. Esse caso representa o primeiro item da imagem de medição de tempo.

UNASSIGN: <gf_ekko>.
LOOP AT gt_ekko
ASSIGNING <gf_ekko>.
    UNASSIGN: <gf_ekpo>.
    LOOP AT gt_ekpo
    ASSIGNING <gf_ekpo>.
        IF <gf_ekko>-ebeln = <gf_ekpo>-ebeln.
            gs_lista-ebeln = <gf_ekko>-ebeln.
            gs_lista-qtd = 1.
            COLLECT gs_lista INTO gt_lista.
        ENDIF.
    ENDLOOP.
ENDLOOP.

2 - Loop segundo grau - para poucos ou um pouco mais de poucos registros. Nesse caso, o segundo loop contém um WHERE, o que permite uma localização rápida do registro inicial que contém os dados necessários. Esse caso representa o segundo item da imagem de medição de tempo.

UNASSIGN: <gf_ekko>.
LOOP AT gt_ekko
ASSIGNING <gf_ekko>.
    UNASSIGN: <gf_ekpo>.
    LOOP AT gt_ekpo
    ASSIGNING <gf_ekpo>
      WHERE ebeln = <gf_ekko>-ebeln.
        IF <gf_ekko>-ebeln = <gf_ekpo>-ebeln.
            gs_lista-ebeln = <gf_ekko>-ebeln.
            gs_lista-qtd = 1.
            COLLECT gs_lista INTO gt_lista.
        ENDIF.
    ENDLOOP.
ENDLOOP.

3 - Loop plus plus - para poucos ou muitos dados. Este processo usa no início do segundo loop um read table com a opção transporting no fields, o que permitem um posicionamento inicial do dados desejado e transporta essa posição para o segundo loop usando a opção from sy-tabix. Esse processo se torna mais rápido porque usa o posicionamento do índice dos registros para localizar as informações e não propriamente os dados dos registros. Esse caso representa o terceiro item da imagem de medição de tempo.

UNASSIGN: <gf_ekko>.
LOOP AT gt_ekko
ASSIGNING <gf_ekko>.

    READ TABLE gt_ekpo
    TRANSPORTING NO FIELDS
      WITH KEY ebeln = <gf_ekko>-ebeln
        BINARY SEARCH.

    IF sy-subrc EQ 0.
        UNASSIGN: <gf_ekpo>.
        LOOP AT gt_ekpo
        ASSIGNING <gf_ekpo>
          FROM sy-tabix.
            IF <gf_ekko>-ebeln = <gf_ekpo>-ebeln.
                gs_lista-ebeln = <gf_ekko>-ebeln.
                gs_lista-qtd = 1.
                COLLECT gs_lista INTO gt_lista.
            ELSE.
                EXIT.
            ENDIF.
        ENDLOOP.
    ENDIF.
ENDLOOP.

Como pode ser notado, o loop mais elaborado, que permite uma melhor performance em qualquer dos casos, não é tão mais complexo ou trabalhoso para ser criado. Agora a escolha de utilização fica a gosto de cada um.

Seguem mais algumas dicas para sempre serem usadas para se trabalhar com grandes quantidade de dados:
  1. SORT: sempre ordene os seus dados para permitir uma melhor performance

  2. BINARY SEARCH: sempre use o binary search no read table. Ele permite uma melhor performance para obter a informação desejada. Obs: sempre ordene os dados baseado nos campos que serão usados como chave no read table, pois caso os dados estejam usando outra ordem e o comando read table esteja usando o binary search, o retorno poderá ser informado como não encontrado, por mais que a informação exista. Vejam o conceito da busca binária aqui (é bem interessante e vale a pena o conhecimento e entendimento).

Antes que eu esqueça, vocês podem reparar que eu usei FIELD-SYMBOLS em todos os exemplo, ao invés de WORKAREAS ou HEADERS. Isso por um simples motivos: porque workareas e headers sempre (sempre) apresentam performance menor, para qualquer situação. Depois monto um post explicando o porque usar field-symbols.

Caso se interessem, segue abaixo os arquivos dos reports utilizados para medição: fontes.

Até mais e obrigado pelos peixes.

Nenhum comentário:

Postar um comentário