ABAPプログラムのパフォーマンス改善

言うまでもなく、どんなプログラムであれパフォーマンス(処理時間)は非常に大切な要素です。
パフォーマンスが悪い場合、オンラインのプログラムであれば使うユーザーにストレスを与えますし、
バッチ処理であればJOBが遅延して後続のJOBに影響を与えたり、業務開始時にJOBが終わっていないなどの
弊害もあり得ます。

プログラムが遅いことが発覚したら、まずはどの処理がネックになっているかを調べることです。

[参考：ABAPプログラムのパフォーマンスの調べ方](https://qiita.com/Auggie_2345/items/77bc1b4fd943f192b83f)

その上で、次のような問題が発覚したらリファクタリングすることを推奨します。

 - LOOPの粒度とSELECTの粒度が一致していない
 - LOOP中に都度SELECT SINGLEしている
 - INDEXが効いていない

---
### 特記事項

* この記事はある程度ABAPのコードが読める人を対象としています。

* この記事はDBがOracleまたはSQLServerである事を前提としています。
インメモリDBであるHANAだと事情は異なると思いますので、その点ご注意ください。

---


## LOOPの粒度とSELECTの粒度が一致していない
SELECTすべき粒度よりもLOOPの粒度の方が細かい場合、無駄なDBアクセスが
発生しています。

```ABAP
SELECT-OPTIONS:
  S_VBELN FOR GDW_VBELN.
SELECT VBAK~VBELN
       VBAK~KUNNR
       VBAP~POSNR
       VBAP~MATNR
INTO TABLE GDT_SALES
FROM VBAK
INNER JOIN VBAP
    ON VBAK~VBELN = VBAP~VBELN.
WHERE VBAK~VBELN IN S_VBELN.

LOOP AT GDT_SALES INTO GDS_SALES.

  CLEAR GDW_LAND1.

  SELECT SINGLE LAND1
  INTO GDW_LAND1
  FROM KNA1
  WHERE KUNNR = GDS_SALES-KUNNR.

  GDS_DATA-VBELN = GDS_SALES-VBELN.
  GDS_DATA-KUNNR = GDS_SALES-KUNNR.
  GDS_DATA-LAND1 = GDW_LAND1.
  GDS_DATA-POSNR = GDS_SALES-POSNR.
  GDS_DATA-MATNR = GDS_SALES-MATNR.

  APPEND GDS_DATA INTO GDT_DATA.

ENDLOOP.
```

↑のコードの何がまずいのかというと、不必要な粒度でKNA1のテーブルに触ってしまっていることです。
内部テーブルのGDT_SALESはVBAKとVBAPをJOINしているので、VBELNとPOSNRの組み合わせで一意です。
KNA1をSELECTするキーであるKUNNRは、VBELNに従属しています。
つまり、VBELNの粒度で持っているデータを、VBELNとPOSNRの組み合わせ単位にSELECTしているのです。

例えば受注伝票のヘッダーが10000レコード、それぞれの受注ごとに明細が3レコードあるとします。
この場合、本来ならばKNA1へのアクセス回数は最大でも10000回で良いはずなのに、↑のコーディングでは30000回アクセスしている事になります。

まあパフォーマンス悪いですよね。
なので↓のようなコードを推奨します。

```ABAP
SELECT-OPTIONS:
  S_VBELN FOR GDW_VBELN.
SELECT VBELN
       KUNNR
INTO TABLE GDT_VBAK
FROM VBAK
WHERE VBELN IN S_VBELN.

SELECT VBELN
       POSNR
       MATNR
INTO TABLE GDT_VBAP
FROM VBAP
WHERE VBELN IN S_VBELN.

LOOP AT GDT_VBAK INTO GDS_VBAK.

  CLEAR GDW_LAND1.

  SELECT SINGLE LAND1
  INTO GDW_LAND1
  FROM KNA1
  WHERE KUNNR = GDS_VBAK-KUNNR.

  LOOP AT GDT_VBAP INTO GDS_VBAP
    WHERE VBELN = GDS_VBAK-VBELN.

    GDS_DATA-VBELN = GDS_VBAK-VBELN.
    GDS_DATA-KUNNR = GDS_VBAK-KUNNR.
    GDS_DATA-LAND1 = GDW_LAND1.
    GDS_DATA-POSNR = GDS_VBAP-POSNR.
    GDS_DATA-MATNR = GDS_VBAP-MATNR.

    APPEND GDS_DATA INTO GDT_DATA.

  ENDLOOP.
ENDLOOP.
```
こうすれば、KNA1にアクセスする回数は最大でも10000回で良くなります。
このようにSELECTするべき粒度とLOOPの粒度を一致させた方がいいです。
ただ、これでもGDT_VBAKをLOOPする都度KNA1に触ってしまっているので、後述するREAD TABLEのコーディングに置き換えると尚いいです。

## LOOP中に都度SELECT SINGLEしている
内部テーブルをLOOPしながら他のテーブルを順次キー読みしていくコーディングは、とてもよく見かけます。
直感的にわかりやすく、コードの記述量も少ないためでしょう。
しかし内部テーブルのレコード数の回数だけ、都度DBアクセスしているのですから、
たとえSELECT SINGLEでキー読みしていたとしても、パフォーマンスは悪化します。
特にバッチ処理のように大量の内部テーブルを扱うプログラムの場合、パフォーマンスがより劣悪になります。

アンチパターンとしては、次のような取り方です。

```ABAP
* VBAKをSELECT
SELECT VBELN KUNNR
FROM VBAK 
INTO TABLE GDT_VBAK
WHERE VBELN IN S_VBELN.

LOOP AT GDT_VBAK INTO GDS_VBAK.

  CLEAR GDW_LAND1.

* KNA1をSELECT(キー読み)
  SELECT SINGLE LAND1
  INTO GDW_LAND1
  FROM KNA1
  WHERE KUNNR = GDS_VBAK-KUNNR.

  GDS_DATA-VBELN = GDS_VBAK-VBELN.
  GDS_DATA-KUNNR = GDS_VBAK-KUNNR.
  GDS_DATA-LAND1 = GDW_LAND1.

  APPEND GDS_DATA INTO GDT_DATA.

ENDLOOP.
```
LOOP中、KNA1のレコードを都度都度SELECTしています。
でも必ずしもそうしなければならないわけではありません。

例えば受注件数が10000件あるとして、受注している得意先が10種類
だけだとします。
↑のコーディングだと、10種類のレコードを取得するのに10000回
実テーブルをSELECTしていることになります。
これではいくらSELECT SINGLEでキー読みしていたとしても、パフォーマンスが
悪くなってしまいます。

例えるならこれは、毎日その日分の食材をスーパーに買いに行っているのと同じようなものです。
普通は向こう何日か分の食材をまとめて買っておきたいですよね。

↑のコードを改善するとしたら以下のようなコードを推奨します。

```ABAP
* VBAKをSELECT
SELECT VBELN KUNNR
FROM VBAK 
INTO TABLE GDT_VBAK
WHERE VBELN IN S_VBELN.

* VBAKをTMPの(一時的な)テーブルに移す
GDT_VBAK_TMP[] = GDT_VBAK[].

* TMPのテーブルをソートする(2020/6/11追記)
SORT GDT_VBAK_TMP BY KUNNR ASCENDING.

* TMPのテーブルをKUNNRで一意にする
DELETE ADJACENT DUPLICATES FROM GDT_VBAK_TMP
COMPARING KUNNR.

* TMPのテーブルが空でないか確認する。
* FOR ALL ENTRIESをするのであれば必ずやったほうがいい。
CHECK GDT_VBAK_TMP IS NOT INITIAL.

* TMPテーブルのKUNNRで、KNA1のレコードを一括で取得する
* GDT_KNA1は後でBINARY SAERCHするため、ORDER BY句でSORTされた状態にする。
SELECT KUNNR
       LAND1
INTO GDT_KNA1
FROM KNA1
FOR ALL ENTRIES IN GDT_VBAK_TMP
WHERE KUNNR = GDT_VBAK_TMP-KUNNR
ORDER BY KUNNR.

*TMPテーブルはもう使わないので、メモリを開放してあげる。
REFRESH GDT_VBAK_TMP.

LOOP AT GDT_VBAK INTO GDS_VBAK.

* KNA1の内部テーブルをREADする。
  READ TABLE GDT_KNA1 INTO GDS_KNA1
  WITH KEY KUNNR = GDS_VBAK-KUNNR
  BINARY SEARCH.

  GDS_DATA-VBELN = GDS_VBAK-VBELN.
  GDS_DATA-KUNNR = GDS_VBAK-KUNNR.
  GDS_DATA-LAND1 = GDS_KNA1-LAND1.

  APPEND GDS_DATA INTO GDT_DATA.

```
これだと、KNA1にアクセスする回数は1回でよく、LOOP中はメモリにある内部テーブルへのアクセスで事足ります。
コード量がおおくなってしまうのがネックですが、パフォーマンスは向上します。
ただし以下の点で注意が必要です。

* 注意点1

FOR ALL ENTRIES命令を使う時は、`CHECK GDT_VBAK_TMP IS NOT INITIAL.`で検索に使う内部テーブルが空でない事を確認した方がいいです。

>ABAPのSQL文のオプションであるfor all entries命令は
内部テーブルにある値を使って一括して検索ができる
便利な命令です。

>ただし、注意点は指定した内部テーブルが0件の場合は
全件検索となってしまうことです。

>大抵の場合、内部テーブルが0件の場合は対象データがない
ということで検索しないことが正しいはずなので、
for all entriesを使うSQLを書く場合はその前に
内部テーブルのエントリ数チェックを必ずかける
習慣にしたほうがいいと思います。
>http://sapabap.seesaa.net/article/385713019.html

* 注意点2

内部テーブルをBINARY SEARCHする時は、必ず内部テーブルをSORTする点に注意。

[参考：ソートしていない内部テーブルをバイナリサーチしてはいけない](https://qiita.com/Auggie_2345/items/c4eafed3d130eaa99a95)

↑のコードだとKNA1のSELECTの時に`ORDER BY KUNNR.`でソートしています。
あるいは`LOOP AT GDT_VBAK INTO GDS_VBAK.`の前でSORT命令を使うか、
それか`GDT_KNA1`をSORTED TABLEで宣言するのも手です。

* 注意点3 2020/6/11 追記

`SORT GDT_VBAK_TMP BY KUNNR ASCENDING.`
内部テーブルの重複を削除する際は、事前にソートされていないと正しく重複削除されません。
[参考：内部テーブルの重複を削除する時は事前にソートする事](https://qiita.com/Auggie_2345/items/218d56bf0828208de1ff)

## INDEXが効いていない
INDEXが効いておらず、テーブルへのフルスキャンが走っている処理があるのなら、
そこがボルトネックになっている可能性は非常に高いです。
特にテーブルを外部キーで読む場合には、INDEXが使われていないとフルスキャンになる可能性が高いです。
例えば、
受注伝票ヘッダー(VBAK)を得意先発注番号(BSTNK)でSELECTする、
受注伝票明細(VBAP)を品目コード(MATNR)でSELECTする、
請求伝票ヘッダー(VBRK)を取消請求伝票(SFAKN)でSELECTする、
といった場合です。

INDEXが効いているかどうかは、下記の方法で調査可能です。
[参考：ABAPプログラムのパフォーマンスの調べ方](https://qiita.com/Auggie_2345/items/77bc1b4fd943f192b83f)

INDEXは次の方法で設定可能です。
TCODE:SE11
索引>ディクショナリ:索引


以上