Lesson 4 of 6
Table expressions and internal table operations
11 min
Listen to this lesson
Classic ABAP reads internal tables with READ TABLE, then checks sy-subrc. Modern ABAP reads a row inside an expression: agreements[ id = agr_id ]. It is shorter and can be used anywhere an operand is expected. It also behaves differently when the row is missing, and that difference is the most common regression in syntax refactoring. This lesson covers table expressions, the built-in functions around them, and iteration with FOR and LOOP AT ... GROUP BY.
Reading a row
| Need | Classic | Modern |
|---|---|---|
| Row by key, must exist | READ TABLE lt_agr INTO ls_agr WITH KEY id = lv_id.IF sy-subrc <> 0. RAISE ... ENDIF. | DATA(agr) = agreements[ id = agr_id ]. |
| Row may be missing | READ TABLE ... then IF sy-subrc = 0 | DATA(agr) = VALUE #( agreements[ id = agr_id ] OPTIONAL ). |
| Default if missing | READ TABLE plus IF ... ELSE | VALUE #( rates[ product = p ]-rate DEFAULT 45 ) |
| Only check existence | READ TABLE ... TRANSPORTING NO FIELDS. | IF line_exists( agreements[ id = agr_id ] ). |
| Row number | READ TABLE ... | then sy-tabix DATA(idx) = line_index( agreements[ id = agr_id ] ). |
| Change a field in place | READ TABLE ... ASSIGNING <fs>.<fs>-status = ... | agreements[ id = agr_id ]-status = 'CLOSED'. |
Classic READ TABLE patterns and their table expression equivalents.
The key difference: if the row does not exist, a table expression raises the exception CX_SY_ITAB_LINE_NOT_FOUND. READ TABLE only set sy-subrc to 4. Classic code that ignored sy-subrc carried on with an empty work area; refactored code dumps, unless it uses OPTIONAL, DEFAULT, line_exists or catches the exception. Which one is right depends on the business rule, and that is a question for the process owner, not the compiler.
Keys and performance
A table expression uses the table key when the condition matches it, and otherwise searches linearly, exactly like READ TABLE ... WITH KEY on a standard table. For a hashed or sorted table, or a secondary key named with KEY, lookups are fast. Contoso's legacy report read 3,000 customers per agreement row with a linear search; giving the customer table a hashed key fixed a runtime problem that predated the conversion. Two other built-ins replace classic statements: lines( itab ) instead of DESCRIBE TABLE, and line_exists( ) instead of TRANSPORTING NO FIELDS.
Secondary keys deserve a sentence of their own. A table can have one primary key and several secondary keys, declared in the type with WITH NON-UNIQUE SORTED KEY by_status COMPONENTS status or a hashed variant. Table expressions, FILTER and LOOP AT ... USING KEY can then name the key, and the runtime keeps the key's index up to date. For Contoso's report, a standard table of agreements with a sorted secondary key on status lets the code read by agreement ID in order and filter by status quickly, without sorting the table twice.
Iteration expressions
FOR inside VALUE builds one table from another: DATA(open_ids) = VALUE string_table( FOR a IN agreements WHERE ( status = 'OPEN' ) ( CONV string( a-id ) ) ). LOOP AT ... GROUP BY groups rows by a key, for example agreements by customer, and lets you loop over the members of each group. Use them to express a transformation, not to cram complex logic into one statement; a LOOP with clear steps is still fine when the body does real work.
Walkthrough: refactoring a lookup
- Find the legacy block in
ZCMC_CORE_RET_LIST:READ TABLE lt_rates INTO ls_rate WITH KEY product = ls_item-product.followed byls_out-rate = ls_rate-rate.with no sy-subrc check. - Ask what happens today when the rate is missing: the work area keeps the previous row's rate, a silent defect. Maria confirms the business rule: a missing rate means 45.00, the standard deposit.
- Write a characterization test for the current behavior and a test for the agreed rule (missing rate gives 45.00).
- Refactor to
out-rate = VALUE #( rates[ product = item-product ]-rate DEFAULT 45 ). - Give
ratesa sorted or hashed key on product if the table is large. - Run the tests; the characterization test now fails on purpose because the old defect is fixed. Record that as an intended change.
What goes wrong in testing
- New dumps. A table expression without OPTIONAL in a path where the row can be missing turns an old silent defect into a short dump in production.
- Stale work areas fixed by accident. Refactoring changes output; without a characterization test nobody can say whether that was intended.
- Hidden performance. Repeated table expressions on a standard table in a loop are linear searches each time.
- Changing in place.
agreements[ ... ]-status = 'CLOSED'changes the table row directly, like a field symbol; a test that only checks the return value misses it.
What would you do?
After Kenji refactors a READ TABLE into a table expression, the nightly test run in CMQ shows a short dump CX_SY_ITAB_LINE_NOT_FOUND in the deposit report for one customer. What should Kenji do?
- A table expression raises CX_SY_ITAB_LINE_NOT_FOUND when the row is missing; READ TABLE only set sy-subrc.
- Use OPTIONAL, DEFAULT, line_exists( ) or a TRY block, depending on the business rule.
- Keys decide speed: hashed, sorted or secondary keys avoid linear searches.
- Characterization tests show when a refactoring changes output; agree intended changes with the key user.
Check your understanding · not graded
Lesson check
1. What happens when agreements[ id = 99 ] finds no row?
2. Which built-in replaces READ TABLE ... TRANSPORTING NO FIELDS for an existence check?
3. How do you get 45.00 when the rate row is missing?
4. Why can repeated table expressions in a loop be slow?
5. A refactoring fixes a stale-work-area defect. What should happen?