Performance and Best Practices for ABAP Reports
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.
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
| Rule | Why |
|---|---|
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 LOOP | The database discards rows before they are sent. |
Use key or index fields in WHERE | Otherwise the database reads the whole table. Check indexes in SE11. |
Never put a SELECT inside a LOOP | One database call per row. The classic performance killer. |
Use INTO TABLE, not SELECT ... ENDSELECT | One transfer instead of many small ones. |
| Let the database calculate | Use 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.
- Check the driver table is not empty. An empty driver table makes
FOR ALL ENTRIESselect all rows of the table. - Remove duplicates from the driver table. Fewer entries, fewer database calls.
- Select the key fields.
FOR ALL ENTRIESremoves duplicate result rows, so you can lose rows if the selected fields are not unique.
JOIN or FOR ALL ENTRIES?
JOIN | FOR ALL ENTRIES | |
|---|---|---|
| Where the work is done | In the database, in one statement | In ABAP, split into several statements |
| Good for | Two or three related tables, filter on both | Data that is already in an internal table |
| Watch for | Very large joins, missing join conditions | Empty 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
| Type | Read by key | Notes |
|---|---|---|
STANDARD | Linear search, or fast with BINARY SEARCH after SORT | Best for appending and looping |
SORTED | Fast (binary search on the key) | Stays sorted automatically. Good for partial-key reads. |
HASHED | Fastest, almost constant time | Unique 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.
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,
INTOis fine and often clearer.
5. SELECT SINGLE vs. UP TO 1 ROWS
| Statement | Use when |
|---|---|
SELECT SINGLE | You supply the full primary key. The result is exactly one row. |
UP TO 1 ROWS | The 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.
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.
| Rule | Detail |
|---|---|
SELECT DISTINCT, ORDER BY | Done in the database, but costly on large sets. Use only when needed. |
| Buffered tables | Small configuration tables are read from memory. Add BYPASSING BUFFER only when you need the very latest data. |
COMMIT WORK | Never inside a loop. It is expensive and releases locks and cursors. |
FREE | Release large internal tables you no longer need. |
UP TO n ROWS | Use 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.
| Prefix | Meaning | Example |
|---|---|---|
lv_ / gv_ | Local / global variable | lv_total |
ls_ / gs_ | Structure | ls_vbak |
lt_ / gt_ | Internal table | lt_vbak |
lo_ / go_ | Object reference | lo_report |
mv_ / mt_ | Class attribute (member) | mt_vbak |
iv_ it_ ev_ et_ rv_ | Method parameters: importing, exporting, returning | iv_vbeln |
lcl_ / zcl_ | Local / global class | lcl_report |
c_ / lc_ | Constant | c_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
| Tool | Use it to |
|---|---|
/h and breakpoints | Step through code. Type /h in the command field, or set a breakpoint in the editor. |
| Watchpoints | Stop when a variable changes (“stop when lv_total changes”). |
ST05 SQL trace | Find slow or repeated SELECT statements. |
SAT runtime analysis | Find slow ABAP code and where the time goes. Older name: SE30. |
ST12 | Combined ABAP and SQL trace, if available in your system. |
ST22 | Read short dumps. |
SCI / ATC | Static checks that flag performance and style problems before transport. |
Using ST05, step by step
- Open
ST05and tick SQL trace. Click Activate Trace. - In another session, run your report.
- Back in
ST05, click Deactivate Trace, then Display Trace. - Sort by duration. Look for statements that appear hundreds of times (a
SELECTin a loop) or take very long.
10. Slow report to fast report
- Reproduce the problem with production-like data and selection values.
- Trace it with
SATfor ABAP time andST05for database time. - Pick the top three offenders. Usually a repeated
SELECT, a nested loop, or an unselectiveWHERE. - Refactor one thing at a time with the rules in this blog.
- 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
ST05on one of your reports and find the slowestSELECT. - Add the
IS NOT INITIALcheck before aFOR ALL ENTRIES. - Rename the variables in Program 1 to follow the naming table above.
13. Common mistakes
FOR ALL ENTRIESwith an empty driver table. It reads the entire table.SELECTinside aLOOP. One database call per row.BINARY SEARCHwithoutSORT. 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 likeVBAPorMSEG. COMMIT WORKinside 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.
WhatsApp us