Internal Tables Deep Dive — SORT, READ, MODIFY, DELETE, COLLECT

Internal Tables Deep Dive: SORT, READ, MODIFY, DELETE, COLLECT
ABAP Reports Series · Blog 4 of 15

Internal Tables Deep Dive: SORT, READ, MODIFY, DELETE, COLLECT

Beginner to intermediate · 20 min read · Needs Blog 3

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.

TypeHow rows are keptBest for
STANDARDIn the order added. Accessed by index.Most reports, lists, ALV output
SORTEDAlways sorted by key automatically.Ordered data, partial key reads
HASHEDBy 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.

Header lines are obsolete

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

StatementWhat 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
COLLECT rules

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 TABLEMeaning
sy-subrc = 0Row found. sy-tabix holds its index.
sy-subrc <> 0Row 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

StatementRowsMemory
CLEAR lt.RemovedKept for reuse
REFRESH lt.Removed (same as CLEAR for tables without header line)Kept
FREE lt.RemovedReleased 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
Two statements depend on SORT

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-tabix is the current row number inside the loop.
  • After the loop, sy-subrc = 0 means at least one row was processed. 4 means the table (or filter) had no rows.
  • INTO copies the row. ASSIGNING avoids 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' ] ).
A missing row causes a short dump

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.

EventRuns
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
Watch the work area inside AT blocks

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 vbtyp resets the counter when a new category starts.
  • Every row adds 1 to the counter.
  • AT END OF vbtyp runs on the last row of the group. SUM totals NETWR for 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 COLLECT to total a numeric field per category.
  • Change the grouping report to group by ERDAT instead.

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 ASSIGNING would 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.

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