Performance & Best Practices

Performance and Best Practices for ABAP Reports
ABAP Reports Series · Blog 13 of 15

Performance and Best Practices for ABAP Reports

Intermediate · 24 min read · Needs Blogs 3, 4 and 12

1. Why performance matters

  • A report that takes 2 seconds on 100 test rows can take hours on millions of production rows.
  • Development systems hold little data, so problems stay hidden until go-live.
  • The database is almost always the slowest part. Read less data, and read it fewer times.
Rule number one

Measure before you optimize. Guessing which line is slow wastes time. Use the tools in section 9 to find the real cause, then fix that.

2. SELECT golden rules

RuleWhy
Name the fields you need, avoid SELECT *Less data crosses the network. Wide tables like VBAP and MSEG have many columns.
Filter in WHERE, not in a LOOPThe database discards rows before they are sent.
Use key or index fields in WHEREOtherwise the database reads the whole table. Check indexes in SE11.
Never put a SELECT inside a LOOPOne database call per row. The classic performance killer.
Use INTO TABLE, not SELECT ... ENDSELECTOne transfer instead of many small ones.
Let the database calculateUse SUM, COUNT, GROUP BY in SQL (section 7).

SELECT inside a loop: before and after

" BAD: one database call per order
LOOP AT lt_orders INTO DATA(ls_order).
  SELECT SINGLE name1 FROM kna1
    WHERE kunnr = @ls_order-kunnr
    INTO @DATA(lv_name).
ENDLOOP.

" GOOD: one call, then fast lookups in memory
SELECT kunnr, name1 FROM kna1
  FOR ALL ENTRIES IN @lt_orders
  WHERE kunnr = @lt_orders-kunnr
  INTO TABLE @DATA(lt_cust).

FOR ALL ENTRIES: three rules

IF lt_orders IS NOT INITIAL.
  SORT lt_orders BY kunnr.
  " (copy only the key column into a driver table, then remove duplicates)
  DATA(lt_driver) = VALUE ty_kunnr_t( FOR ls IN lt_orders ( kunnr = ls-kunnr ) ).
  SORT lt_driver BY kunnr.
  DELETE ADJACENT DUPLICATES FROM lt_driver COMPARING kunnr.

  SELECT kunnr, name1 FROM kna1
    FOR ALL ENTRIES IN @lt_driver
    WHERE kunnr = @lt_driver-kunnr
    INTO TABLE @DATA(lt_cust).
ENDIF.
The three rules
  • Check the driver table is not empty. An empty driver table makes FOR ALL ENTRIES select all rows of the table.
  • Remove duplicates from the driver table. Fewer entries, fewer database calls.
  • Select the key fields. FOR ALL ENTRIES removes duplicate result rows, so you can lose rows if the selected fields are not unique.

JOIN or FOR ALL ENTRIES?

JOINFOR ALL ENTRIES
Where the work is doneIn the database, in one statementIn ABAP, split into several statements
Good forTwo or three related tables, filter on bothData that is already in an internal table
Watch forVery large joins, missing join conditionsEmpty driver table, duplicates

A join of two or three tables on key fields is often faster than FOR ALL ENTRIES. When in doubt, trace both (section 9).

3. Internal table types

TypeRead by keyNotes
STANDARDLinear search, or fast with BINARY SEARCH after SORTBest for appending and looping
SORTEDFast (binary search on the key)Stays sorted automatically. Good for partial-key reads.
HASHEDFastest, almost constant timeUnique key, accessed by key only (no index)

Pattern: load reference data (customers, materials) into a HASHED table once, then read it inside the main loop.

DATA lt_cust_h TYPE HASHED TABLE OF ty_cust WITH UNIQUE KEY kunnr.
lt_cust_h = CORRESPONDING #( lt_cust ).

LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
  READ TABLE lt_cust_h ASSIGNING FIELD-SYMBOL(<ls_cust>)
    WITH TABLE KEY kunnr = <ls_order>-kunnr.
  IF sy-subrc = 0.
    <ls_order>-name1 = <ls_cust>-name1.
  ENDIF.
ENDLOOP.
Do not use HASHED everywhere

A hashed table has no order and needs a unique key. If you must keep rows in sequence, allow duplicate keys, or access by index, use STANDARD or SORTED.

4. Field symbols vs. work areas

LOOP AT ... INTO ls copies every row into a work area. ASSIGNING <fs> points at the row in the table without copying.

" Copies each row (slower for wide rows)
LOOP AT lt_orders INTO DATA(ls_order).
  ...
ENDLOOP.

" References each row (no copy)
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
  <ls_order>-netwr = <ls_order>-netwr * 2.   " changes the table directly
ENDLOOP.
  • The gain is largest with wide rows and large tables.
  • Changes through <fs> modify the table immediately. That is a benefit when intended and a bug when not.
  • If you only read small rows, INTO is fine and often clearer.

5. SELECT SINGLE vs. UP TO 1 ROWS

StatementUse when
SELECT SINGLEYou supply the full primary key. The result is exactly one row.
UP TO 1 ROWSThe key is partial and any matching row is acceptable.
UP TO n ROWS + ORDER BY“Top n” queries, such as the 10 newest orders.
" Top 10 newest orders: database sorts, then limits
SELECT vbeln, erdat FROM vbak
  ORDER BY erdat DESCENDING
  INTO TABLE @DATA(lt_top)
  UP TO 10 ROWS.

6. Avoiding nested loops

A loop inside a loop over two 10,000-row tables runs 100 million times. There are two standard fixes.

Fix 1: hashed or sorted lookup

Replace the inner loop by one read, as in section 3.

Fix 2: parallel cursor

When the inner table has several rows per outer row (orders and their items), sort both tables by the key and start each inner loop where the matching rows begin.

SORT: lt_orders BY vbeln,
      lt_items  BY vbeln.

LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_order>).

  " Find where this order's items start
  READ TABLE lt_items TRANSPORTING NO FIELDS
    WITH KEY vbeln = <ls_order>-vbeln BINARY SEARCH.
  CHECK sy-subrc = 0.
  DATA(lv_start) = sy-tabix.

  " Loop only over this order's items, then stop
  LOOP AT lt_items ASSIGNING FIELD-SYMBOL(<ls_item>) FROM lv_start.
    IF <ls_item>-vbeln <> <ls_order>-vbeln.
      EXIT.
    ENDIF.
    " ... process the item ...
  ENDLOOP.

ENDLOOP.
Both tables must be sorted

Sort by the same key you search. Without SORT, BINARY SEARCH silently returns wrong results.

7. Other performance rules

Aggregate in the database

" BAD: loop and add up in ABAP
" GOOD: let the database do it
SELECT waerk, COUNT( * ) AS cnt, SUM( netwr ) AS total
  FROM vbak
  WHERE erdat IN @s_erdat
  GROUP BY waerk
  INTO TABLE @DATA(lt_summary).

Very large result sets: PACKAGE SIZE

SELECT vbeln, posnr, matnr FROM vbap
  INTO TABLE @DATA(lt_pack) PACKAGE SIZE 50000.
  " process this block of up to 50,000 rows
ENDSELECT.

Only one package is in memory at a time, so a huge table does not exhaust memory.

RuleDetail
SELECT DISTINCT, ORDER BYDone in the database, but costly on large sets. Use only when needed.
Buffered tablesSmall configuration tables are read from memory. Add BYPASSING BUFFER only when you need the very latest data.
COMMIT WORKNever inside a loop. It is expensive and releases locks and cursors.
FREERelease large internal tables you no longer need.
UP TO n ROWSUse while testing, so a mistake does not read millions of rows.

8. Readability and naming

Fast code that nobody can read becomes slow code later, because nobody dares change it.

PrefixMeaningExample
lv_ / gv_Local / global variablelv_total
ls_ / gs_Structurels_vbak
lt_ / gt_Internal tablelt_vbak
lo_ / go_Object referencelo_report
mv_ / mt_Class attribute (member)mt_vbak
iv_ it_ ev_ et_ rv_Method parameters: importing, exporting, returningiv_vbeln
lcl_ / zcl_Local / global classlcl_report
c_ / lc_Constantc_vbtyp_order
  • Constants instead of magic values: CONSTANTS c_vbtyp_order TYPE vbak-vbtyp VALUE 'C'. instead of a bare 'C'.
  • TYPES instead of repeated structures: define a type once and reuse it.
  • Short methods: aim for under about 30 lines, doing one thing.
  • Comments explain why, not what the code already says.
  • Follow your company’s naming rules first. These prefixes are the common default.

9. Debugging and analysis tools

ToolUse it to
/h and breakpointsStep through code. Type /h in the command field, or set a breakpoint in the editor.
WatchpointsStop when a variable changes (“stop when lv_total changes”).
ST05 SQL traceFind slow or repeated SELECT statements.
SAT runtime analysisFind slow ABAP code and where the time goes. Older name: SE30.
ST12Combined ABAP and SQL trace, if available in your system.
ST22Read short dumps.
SCI / ATCStatic checks that flag performance and style problems before transport.

Using ST05, step by step

  1. Open ST05 and tick SQL trace. Click Activate Trace.
  2. In another session, run your report.
  3. Back in ST05, click Deactivate Trace, then Display Trace.
  4. Sort by duration. Look for statements that appear hundreds of times (a SELECT in a loop) or take very long.

10. Slow report to fast report

  1. Reproduce the problem with production-like data and selection values.
  2. Trace it with SAT for ABAP time and ST05 for database time.
  3. Pick the top three offenders. Usually a repeated SELECT, a nested loop, or an unselective WHERE.
  4. Refactor one thing at a time with the rules in this blog.
  5. Measure again and compare with the first trace. Keep the numbers.

11. Timing program

This program compares a STANDARD table read with a HASHED read. Run it and see the difference yourself.

REPORT zperf_compare.

TYPES: BEGIN OF ty_row,
         id  TYPE i,
         txt TYPE c LENGTH 10,
       END OF ty_row.

DATA: lt_std  TYPE STANDARD TABLE OF ty_row WITH DEFAULT KEY,
      lt_hash TYPE HASHED TABLE OF ty_row WITH UNIQUE KEY id,
      ls_row  TYPE ty_row,
      lv_key  TYPE i,
      lv_t0   TYPE i,
      lv_t1   TYPE i,
      lv_std  TYPE i,
      lv_hash TYPE i.

START-OF-SELECTION.
  " Build both tables with 50,000 rows
  DO 50000 TIMES.
    ls_row-id  = sy-index.
    ls_row-txt = 'DATA'.
    APPEND ls_row TO lt_std.
    INSERT ls_row INTO TABLE lt_hash.
  ENDDO.

  " 1. STANDARD table: linear search
  GET RUN TIME FIELD lv_t0.
  DO 5000 TIMES.
    lv_key = sy-index * 10.
    READ TABLE lt_std INTO ls_row WITH KEY id = lv_key.
  ENDDO.
  GET RUN TIME FIELD lv_t1.
  lv_std = lv_t1 - lv_t0.

  " 2. HASHED table: direct access
  GET RUN TIME FIELD lv_t0.
  DO 5000 TIMES.
    lv_key = sy-index * 10.
    READ TABLE lt_hash INTO ls_row WITH TABLE KEY id = lv_key.
  ENDDO.
  GET RUN TIME FIELD lv_t1.
  lv_hash = lv_t1 - lv_t0.

  WRITE: / 'STANDARD (microseconds):', lv_std,
         / 'HASHED   (microseconds):', lv_hash.

The numbers depend on your system. The hashed read should be many times faster, and the gap grows with the table size.

12. Try it yourself

  • Take Program 2 and replace a nested loop with a hashed lookup.
  • Time both versions with GET RUN TIME FIELD.
  • Run ST05 on one of your reports and find the slowest SELECT.
  • Add the IS NOT INITIAL check before a FOR ALL ENTRIES.
  • Rename the variables in Program 1 to follow the naming table above.

13. Common mistakes

  • FOR ALL ENTRIES with an empty driver table. It reads the entire table.
  • SELECT inside a LOOP. One database call per row.
  • BINARY SEARCH without SORT. Wrong results with no error.
  • Optimizing without measuring. You fix the wrong thing.
  • HASHED tables for data that needs order or non-unique keys.
  • Selecting * from wide tables like VBAP or MSEG.
  • COMMIT WORK inside a loop.
  • Testing only with small data. Try a realistic volume before transport.

Up next: Blog 14

A Real-World Report. Authority checks, message classes, variants, SUBMIT, and background jobs: what it takes to run a report in production.

ABAP Reports: Zero to Hero · Blog 13 of 15
WhatsApp WhatsApp us
Call Now Button