Internal Tables Deep Dive: SORT, READ, MODIFY, DELETE, COLLECT
1. What is an internal table?
An internal table is a table that lives in the program’s memory while it runs. It is the heart of every ABAP report: you SELECT data into it, change it, and then display it.
| Type | How rows are kept | Best for |
|---|---|---|
STANDARD | In the order added. Accessed by index. | Most reports, lists, ALV output |
SORTED | Always sorted by key automatically. | Ordered data, partial key reads |
HASHED | By a hash of the key. No index. | Fast lookups by unique key |
Blog 13 compares their speed in depth. For now, STANDARD is the default.
In old code you will see tables declared with WITH HEADER LINE or OCCURS 0. Do not use them. Always use a separate work area, as shown in this post.
2. Declaring internal tables
" Based on a database table structure
DATA lt_vbak TYPE STANDARD TABLE OF vbak WITH DEFAULT KEY.
" With a named key
DATA lt_vbak2 TYPE STANDARD TABLE OF vbak WITH NON-UNIQUE KEY vbeln.
" Work area: one row of the table
DATA ls_vbak TYPE vbak.
" Custom structure and table type (best for reports)
TYPES: BEGIN OF ty_order,
vbtyp TYPE vbak-vbtyp,
vbeln TYPE vbak-vbeln,
erdat TYPE vbak-erdat,
netwr TYPE vbak-netwr,
END OF ty_order,
ty_order_t TYPE STANDARD TABLE OF ty_order WITH DEFAULT KEY.
DATA lt_orders TYPE ty_order_t.
DATA ls_order TYPE ty_order.
A custom structure holds only the fields you need, so the table uses less memory than a full VBAK row.
3. Adding rows
| Statement | What it does |
|---|---|
APPEND ls TO lt. | Adds one row at the end |
APPEND LINES OF lt_src TO lt. | Adds all rows of another table |
INSERT ls INTO lt INDEX 1. | Inserts at a position (standard tables) |
INSERT ls INTO TABLE lt. | Inserts at the right place (sorted and hashed tables) |
COLLECT ls INTO lt. | Adds the row, or sums the numbers if the key exists |
APPEND VALUE #( vbtyp = 'C' vbeln = '0000000001' netwr = 500 ) TO lt_orders.
APPEND VALUE #( vbtyp = 'C' vbeln = '0000000002' netwr = 300 ) TO lt_orders.
APPEND VALUE #( vbtyp = 'A' vbeln = '0000000003' netwr = 900 ) TO lt_orders.
COLLECT: add up duplicates
COLLECT looks at the non-numeric fields as the key. If a row with the same key exists, it adds the numeric fields. If not, it appends a new row.
TYPES: BEGIN OF ty_total,
vbtyp TYPE vbak-vbtyp,
netwr TYPE vbak-netwr,
END OF ty_total.
DATA lt_total TYPE STANDARD TABLE OF ty_total WITH DEFAULT KEY.
LOOP AT lt_orders INTO ls_order.
COLLECT VALUE ty_total( vbtyp = ls_order-vbtyp
netwr = ls_order-netwr ) INTO lt_total.
ENDLOOP.
" Result: C = 800, A = 900
The structure must contain only key fields (character-like) and numeric fields (such as CURR, QUAN, I, P). A text field you did not mean as a key will split your totals.
4. Reading rows: READ TABLE
" By position
READ TABLE lt_orders INTO ls_order INDEX 1.
" By a field value
READ TABLE lt_orders INTO ls_order
WITH KEY vbeln = '0000000002'.
" By reference (no copy): faster for wide rows
READ TABLE lt_orders ASSIGNING FIELD-SYMBOL(<ls_order>)
WITH KEY vbeln = '0000000002'.
IF sy-subrc = 0.
WRITE: / 'Found at row', sy-tabix.
ELSE.
WRITE: / 'Not found'.
ENDIF.
BINARY SEARCH
A normal key read checks rows one by one. BINARY SEARCH halves the search range every step, so it is far faster on big tables. It only works if the table is sorted by the same key first.
SORT lt_orders BY vbeln.
READ TABLE lt_orders INTO ls_order
WITH KEY vbeln = '0000000002' BINARY SEARCH.
| After READ TABLE | Meaning |
|---|---|
sy-subrc = 0 | Row found. sy-tabix holds its index. |
sy-subrc <> 0 | Row not found (usually 4, sometimes 8 with binary search). Work area is unchanged. |
5. Modifying rows: MODIFY
" Replace a whole row by index
ls_order-netwr = 750.
MODIFY lt_orders FROM ls_order INDEX 1.
" Change only one field for all rows that match
ls_order-erdat = sy-datum.
MODIFY lt_orders FROM ls_order
TRANSPORTING erdat WHERE vbtyp = 'C'.
" Best inside a loop: change the row directly
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
<ls_order>-netwr = <ls_order>-netwr * 2.
ENDLOOP.
With a field symbol, no MODIFY is needed. The change happens in the table itself.
6. Deleting rows: DELETE
DELETE lt_orders INDEX 1. " by position
DELETE lt_orders WHERE vbtyp = 'A'. " by condition
" Remove duplicates (SORT first!)
SORT lt_orders BY vbeln.
DELETE ADJACENT DUPLICATES FROM lt_orders COMPARING vbeln.
CLEAR vs. REFRESH vs. FREE
| Statement | Rows | Memory |
|---|---|---|
CLEAR lt. | Removed | Kept for reuse |
REFRESH lt. | Removed (same as CLEAR for tables without header line) | Kept |
FREE lt. | Removed | Released to the system |
Use CLEAR to empty a table you will fill again. Use FREE when a large table is no longer needed.
7. Sorting: SORT
SORT lt_orders BY erdat DESCENDING.
SORT lt_orders BY vbtyp ASCENDING netwr DESCENDING.
SORT lt_orders. " uses the table key
SORT lt_orders BY vbtyp STABLE. " keeps old order for equal rows
READ ... BINARY SEARCH and DELETE ADJACENT DUPLICATES give wrong results without errors on an unsorted table. Sort by the same fields you compare.
8. LOOP AT: iterating
LOOP AT lt_orders INTO ls_order. " copies each row
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls>). " references each row
LOOP AT lt_orders INTO ls_order WHERE vbtyp = 'C'.
LOOP AT lt_orders ASSIGNING <ls> FROM 5 TO 10.
sy-tabixis the current row number inside the loop.- After the loop,
sy-subrc = 0means at least one row was processed.4means the table (or filter) had no rows. INTOcopies the row.ASSIGNINGavoids the copy, which matters for wide rows and big tables.
9. Table expressions (ABAP 7.40+)
Read a row directly inside a statement, with square brackets.
DATA(ls_first) = lt_orders[ 1 ]. " by index
DATA(ls_found) = lt_orders[ vbeln = '0000000002' ]. " by key
DATA(lv_value) = lt_orders[ 1 ]-netwr. " one field
lt_orders[ 1 ]-netwr = 999. " write a field
IF line_exists( lt_orders[ vbeln = '0000000009' ] ).
WRITE: / 'Order exists'.
ENDIF.
DATA(lv_idx) = line_index( lt_orders[ vbeln = '0000000002' ] ).
If no row matches, a table expression raises CX_SY_ITAB_LINE_NOT_FOUND. It does not set sy-subrc. Check with line_exists( ) first, or catch the exception.
" Safe option 1: TRY / CATCH
TRY.
DATA(ls_row) = lt_orders[ vbeln = '0000000009' ].
CATCH cx_sy_itab_line_not_found.
WRITE: / 'No such order'.
ENDTRY.
" Safe option 2: OPTIONAL returns an empty row instead
DATA(ls_safe) = VALUE #( lt_orders[ vbeln = '0000000009' ] OPTIONAL ).
10. Field symbols with internal tables
A field symbol is a pointer to data. It does not copy anything.
" Point to one row
ASSIGN lt_orders[ 1 ] TO FIELD-SYMBOL(<ls_first>).
IF sy-subrc = 0.
<ls_first>-erdat = sy-datum.
ENDIF.
" Access a field by its name in a variable
DATA lv_field TYPE string VALUE 'VBELN'.
ASSIGN COMPONENT lv_field OF STRUCTURE ls_order TO FIELD-SYMBOL(<lv_val>).
IF sy-subrc = 0.
WRITE: / <lv_val>.
ENDIF.
11. Practical patterns
Pattern 1: build a lookup table, use it in a loop
SELECT kunnr, name1 FROM kna1
INTO TABLE @DATA(lt_cust_tmp).
DATA lt_cust TYPE HASHED TABLE OF kna1 WITH UNIQUE KEY kunnr.
lt_cust = CORRESPONDING #( lt_cust_tmp ).
LOOP AT lt_sales ASSIGNING FIELD-SYMBOL(<ls_sale>).
READ TABLE lt_cust ASSIGNING FIELD-SYMBOL(<ls_cust>)
WITH TABLE KEY kunnr = <ls_sale>-kunnr.
IF sy-subrc = 0.
<ls_sale>-name1 = <ls_cust>-name1.
ENDIF.
ENDLOOP.
The hashed table finds each customer instantly, so there is no nested loop and no SELECT inside the loop.
Pattern 2: remove duplicates before processing
SORT lt_orders BY vbeln.
DELETE ADJACENT DUPLICATES FROM lt_orders COMPARING vbeln.
Pattern 3: merge two tables by key
SORT lt_items BY vbeln.
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
READ TABLE lt_items INTO DATA(ls_item)
WITH KEY vbeln = <ls_order>-vbeln BINARY SEARCH.
IF sy-subrc = 0.
<ls_order>-matnr = ls_item-matnr.
ENDIF.
ENDLOOP.
Pattern 4: group and sum
Use COLLECT (section 3) for simple totals, or AT NEW / AT END OF (next section) for subtotals with counts.
12. Grouping: AT NEW and AT END OF
These control-break events run inside a LOOP when a group of rows starts or ends. The table must be sorted by the group field first, and the group field should be the first column of the structure.
| Event | Runs |
|---|---|
AT FIRST. ... ENDAT. | Once, before the first row |
AT NEW field. ... ENDAT. | When the value of the field (or an earlier column) changes |
AT END OF field. ... ENDAT. | On the last row of each group |
AT LAST. ... ENDAT. | Once, after the last row |
Inside AT NEW and AT END OF, the columns after the group field show * or are cleared. Use SUM inside AT END OF to fill the numeric fields with the group totals. Use a separate variable (like a counter) for anything else.
13. Complete grouping report
Count and total sales documents per document category (VBTYP).
REPORT zitab_grouping.
TYPES: BEGIN OF ty_order,
vbtyp TYPE vbak-vbtyp, " group field FIRST
vbeln TYPE vbak-vbeln,
netwr TYPE vbak-netwr,
END OF ty_order.
DATA lt_orders TYPE STANDARD TABLE OF ty_order WITH DEFAULT KEY.
DATA ls_order TYPE ty_order.
DATA lv_count TYPE i.
START-OF-SELECTION.
SELECT vbtyp, vbeln, netwr
FROM vbak
INTO TABLE @lt_orders
UP TO 1000 ROWS.
IF sy-subrc <> 0.
WRITE: / 'No data found'.
RETURN.
ENDIF.
SORT lt_orders BY vbtyp.
WRITE: / 'Type', 10 'Count', 25 'Total value'.
ULINE.
LOOP AT lt_orders INTO ls_order.
AT NEW vbtyp.
CLEAR lv_count.
ENDAT.
lv_count = lv_count + 1.
AT END OF vbtyp.
SUM.
WRITE: / ls_order-vbtyp, 10 lv_count, 25 ls_order-netwr.
ENDAT.
ENDLOOP.
How it works
- The data is read into an internal table, then sorted by
VBTYP. AT NEW vbtypresets the counter when a new category starts.- Every row adds 1 to the counter.
AT END OF vbtypruns on the last row of the group.SUMtotalsNETWRfor the group, and the line is printed.
14. Try it yourself
- Build a table of 5 rows by hand with
APPEND VALUE #( ... ). - Sort it by one field and read a row with
BINARY SEARCH. - Delete one row and modify another, then print the table.
- Read row 3 using a table expression, with
line_exists( )as a guard. - Use
COLLECTto total a numeric field per category. - Change the grouping report to group by
ERDATinstead.
15. Common mistakes
- Using a header line (
OCCURS 0,WITH HEADER LINE). It is obsolete and confusing. - READ TABLE without checking sy-subrc. You then use an old or empty work area.
- BINARY SEARCH without SORT. Rows are missed and no error is shown.
- DELETE ADJACENT DUPLICATES without SORT. Only neighbours are compared, so duplicates remain.
- Table expression without a guard. A missing row ends the program with a short dump.
- LOOP … INTO on a wide table where
ASSIGNINGwould avoid the copy. - Forgetting to CLEAR the work area before building the next row, so old values leak in.
- Group field not first in the structure when using
AT NEW.
Up next: Blog 5
Your First ALV Report. Time to replace WRITE with a real grid that users can sort, filter, and export.
WhatsApp us