Blog › ICP guides
ABAP developer on retainer: FOR ALL ENTRIES IN empty table, SELECT behavior, S/4HANA ABAP, and SAP ABAP on monthly retainer
October 3, 2026 · ~14 min read
An ABAP developer was maintaining a production planning report at a large manufacturing company running SAP ECC 6.0. The report identified open production orders that needed material replenishment, then selected the relevant purchase requisition records from the database using a SELECT ... FOR ALL ENTRIES IN lt_orders WHERE aufnr = lt_orders-aufnr. The internal table lt_orders was populated earlier in the report with the list of open production order numbers that required replenishment. In normal operation — when open production orders existed — the SELECT returned only the purchase requisition rows from EBAN matching those specific order numbers, and the report processed them correctly.
The developer ran the report at end-of-quarter when no open production orders existed: lt_orders was empty. ABAP’s FOR ALL ENTRIES IN clause has a critical, SAP-specific semantic: when the internal table passed to FOR ALL ENTRIES IN is empty, the clause does not filter the SELECT at all — it returns all rows from the database table. This is documented in SAP Note 71123 and in ABAP programming guidelines, but it is non-obvious and differs fundamentally from what a SQL developer would expect: WHERE column IN (empty list) in standard SQL returns zero rows. The SELECT returned all 1.2 million purchase requisition rows from the EBAN table. The report ran for 4 hours before being killed by a background job timeout.
During the time it ran, 3 monthly production planning aggregations that read from related tables the report was locking produced wrong totals. Wrong monthly aggregations: 3 → 0 after adding an empty-table guard: IF sy-subrc = 0 AND lines(lt_orders) > 0. SELECT ... FOR ALL ENTRIES IN lt_orders ... ENDIF. The fix is three lines. The investigation that identified the cause — reading the report’s SELECT logic, cross-referencing SAP Note 71123, tracing the lock contention to the 3 downstream aggregations, and verifying the guard condition — is 3 hours with nothing to show except a corrected program and a corrected aggregation count.
The reason this is systematically invisible: ABAP developers who learned FOR ALL ENTRIES IN with always-populated internal tables never encounter the empty-table behavior in development or testing. The developer’s test data always includes at least one open production order. The production failure occurs only at end-of-quarter or end-of-year when no orders are pending — a condition that may occur once per year. Every other run is correct. The SAP kernel produces no warning and no error when the internal table is empty: the SELECT completes with SY-SUBRC = 0, signaling success, and returns a very large result set that takes hours to process before the background job watchdog kills the work process.
ABAP: SAP’s application programming language, ECC, and S/4HANA
ABAP (Advanced Business Application Programming) was developed by SAP in the 1980s for R/2, the mainframe-era SAP system, and carried forward into R/3 and all subsequent SAP generations. It is an interpreted language compiled to ABAP byte code and executed by the ABAP runtime within the SAP kernel — a two-layer architecture where the ABAP runtime manages memory, work processes, and database access, sitting above the underlying operating system and database. ABAP is tightly coupled to the SAP Data Dictionary (DDIC), which defines all database tables, structures, domains, and data elements: every table used in a SELECT statement has a corresponding DDIC definition, and the ABAP compiler validates field names and types against DDIC metadata at syntax check time. This coupling means that a developer cannot write a SELECT against a field that does not exist in DDIC without a syntax error — which prevents certain classes of bugs, but means that all schema changes must go through DDIC transport, not direct DDL.
The ABAP development environment has two generations. The ABAP Workbench (SE38 for program editor, SE80 for object navigator) is the traditional browser-based IDE embedded in the SAP GUI; it covers all ABAP development but is a thick-client Windows application. Eclipse-based ABAP Development Tools (ADT) is SAP’s current-generation IDE: an Eclipse plugin that provides code completion, inline syntax checking, and refactoring support via the ADT protocol, connecting to the SAP system over HTTP. ADT is required for certain S/4HANA development tasks such as CDS view creation. SAP ECC 6.0 (Enterprise Central Component) is the traditional ABAP stack; it runs on any SAP-supported database (Oracle, IBM DB2, MSSQL, MaxDB) and supports ABAP up to the version bundled with the ECC enhancement package applied. S/4HANA is SAP’s current generation: it requires the SAP HANA in-memory column-store database exclusively, introduces CDS views as the primary reporting and API layer, supports ABAP 7.4+ language features, and uses the Fiori web UI instead of SAP GUI for end-user screens.
ABAP 7.4 and later introduced several language features that substantially reduce boilerplate. Inline declarations allow DATA(lv_result) = lv_value * 2 instead of a separate DATA lv_result TYPE i. declaration. String templates use |Hello { lv_name }| syntax for string interpolation. Table expressions allow lt_table[ key = lv_key ] to read a single row by key without a READ TABLE statement. The REDUCE operator accumulates a result over an internal table: REDUCE i( INIT sum = 0 FOR row IN lt_table NEXT sum = sum + row-amount ). The FILTER operator produces a filtered copy of an internal table without a LOOP: FILTER #( lt_items WHERE active = abap_true ). These features are available only on systems with ABAP 7.4 or higher; ECC 6.0 without recent enhancement packages may be on ABAP 7.02 or 7.31, where none of these features exist.
The SELECT statement is ABAP’s primary database access mechanism. Key variants: SELECT ... INTO TABLE lt_result fills an internal table; SELECT ... INTO CORRESPONDING FIELDS OF TABLE lt_result maps columns to structure fields by name when the internal table’s structure differs from the database table’s field order or includes extra fields; SELECT SINGLE ... INTO ls_row retrieves at most one row; SELECT COUNT(*) INTO lv_count FROM table WHERE ... counts rows; ORDER BY, GROUP BY, and aggregate functions SUM, MAX, MIN, AVG are available. The FOR ALL ENTRIES IN clause is an ABAP-specific extension: it converts the SELECT into a series of SELECTs or a large IN clause at the database level, depending on the number of entries in the internal table and the SAP kernel version. For large internal tables, FOR ALL ENTRIES IN can cause significant database load — each entry may generate a separate database roundtrip in some kernel configurations. On S/4HANA with HANA, JOINs are generally more efficient because HANA’s optimizer can push the join predicate into the column store engine, whereas FOR ALL ENTRIES IN with thousands of entries generates a very wide IN clause that strains the SQL parser. CDS (Core Data Services) views are HANA-native analytical views defined using a SQL-like DDL syntax and exposed as virtual database objects; in S/4HANA they replace direct ABAP SELECTs for reporting and serve as the consumption layer for Fiori applications and OData services.
ABAP internal tables, ABAP OO, and BAPI/RFC
ABAP internal tables are the central data structure for all in-memory processing. Three types exist, with fundamentally different performance characteristics. STANDARD TABLE: supports index access, allows duplicate key values, has no maintained sort order; READ TABLE without BINARY SEARCH performs a linear scan — O(n) per lookup. Acceptable for small tables; catastrophic for tables with hundreds of thousands of rows where the developer performs repeated keyed reads. SORTED TABLE: maintained in ascending sort order by the declared key; allows binary search automatically on READ TABLE without an explicit BINARY SEARCH addition; cannot contain duplicate key values if declared with UNIQUE KEY; insert operations are O(log n). HASHED TABLE: hash access only, no index access, O(1) key lookup, no duplicate keys; fastest for large tables where all access is by full key; LOOP AT ... WHERE still performs a full scan. A common retainer bug: a developer declares a STANDARD TABLE that accumulates 200,000 rows during a batch job, then calls READ TABLE lt_data WITH KEY matnr = lv_material in a loop for each of 50,000 work items; the 50,000 × 200,000 linear scan produces a job that runs for hours. The fix is declaring the internal table as a HASHED TABLE WITH UNIQUE KEY matnr or adding BINARY SEARCH after a SORT lt_data BY matnr.
ABAP Objects (ABAP OO) is the object-oriented layer introduced in ABAP 6.10. A class is defined with CLASS lcl_processor DEFINITION. ... ENDCLASS. and implemented with CLASS lcl_processor IMPLEMENTATION. ... ENDCLASS. Instance methods are declared in the PUBLIC SECTION, PROTECTED SECTION, or PRIVATE SECTION of the class definition with METHODS method_name IMPORTING iv_param TYPE i RETURNING VALUE(rv_result) TYPE string. Static (class) methods use CLASS-METHODS. Method calls use the object reference: lo_processor->process( iv_input = lv_input ). Interfaces are declared with INTERFACE lif_processor. and implemented with CLASS lcl_impl DEFINITION. INTERFACES lif_processor. ENDCLASS. Exception handling uses TRY. ... CATCH cx_exception INTO lo_exc. WRITE lo_exc->get_text( ). ENDTRY. Exception classes inherit from CX_STATIC_CHECK (caller must handle or declare), CX_DYNAMIC_CHECK (runtime check), or CX_NO_CHECK (can be raised anywhere without declaration). Raising an exception: RAISE EXCEPTION TYPE cx_abap_invalid_value EXPORTING textid = cx_abap_invalid_value=>cx_abap_invalid_value iv_value = lv_bad_value.
Function modules are the legacy ABAP modularization unit, still ubiquitous in all SAP systems. CALL FUNCTION 'FUNCTION_MODULE_NAME' EXPORTING param = value IMPORTING result = lv_result EXCEPTIONS error_type = 1 OTHERS = 2. Remote Function Call (RFC) enables function modules to be called from external systems — other ABAP stacks, Java, .NET, or Python via the SAP RFC library — by marking them as remote-enabled. BAPIs (Business Application Programming Interfaces) are SAP-delivered, stable RFC-enabled function modules for standard business operations: BAPI_GOODSMVT_CREATE for goods movements, BAPI_SALESORDER_CREATEFROMDAT2 for sales order creation, BAPI_PO_CREATE1 for purchase order creation. BAPIs use a specific calling convention: the caller inspects the RETURN parameter table for error messages after the BAPI call, then — critically — calls BAPI_TRANSACTION_COMMIT to commit the SAP Logical Unit of Work (LUW). The LUW is the SAP-level transaction boundary: changes made by a BAPI are buffered in the LUW but not written to the database until the LUW is committed. A developer who inspects the RETURN table, finds no errors, and proceeds without calling BAPI_TRANSACTION_COMMIT will find that no data was persisted — the LUW was rolled back when the calling program ended. The ABAP Test Cockpit (ATC) performs static code analysis, flagging SELECTs without WHERE clauses, SELECT * instead of specific field lists, READ TABLE on STANDARD TABLE without BINARY SEARCH, and missing BAPI_TRANSACTION_COMMIT patterns. ABAP Unit provides a framework for writing test classes (CLASS ltc_test DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.) with test methods annotated with METHODS test_method FOR TESTING.
Typical ABAP retainer work and what it looks like in a work log
The FOR ALL ENTRIES IN empty-table bug is the most invisible category of ABAP retainer work. The pattern is consistent: a developer writes a SELECT with FOR ALL ENTRIES IN against a correctly populated internal table; all test runs with realistic data include at least one entry in the internal table and produce correct results; a specific calendar condition (end-of-quarter, end-of-year, a holiday when no orders were processed) causes the internal table to be empty at report runtime; the SELECT returns the entire contents of a large database table; a background job timeout kills the work process after hours of runtime; downstream jobs that read tables locked by the report produce wrong aggregations or fail. Work log entry: “Z_PP_REPLENISHMENT report: SELECT ebeln ebelp matnr FROM eban INTO TABLE lt_requisitions FOR ALL ENTRIES IN lt_orders WHERE aufnr = lt_orders-aufnr — lt_orders empty at quarter-end; SAP FOR ALL ENTRIES IN empty-table semantics per SAP Note 71123: full table scan returning 1.2M rows from EBAN; report ran 4h before background job timeout; 3 dependent monthly aggregation jobs read lock-contended tables and produced wrong totals; fix: added IF lines(lt_orders) > 0 guard around SELECT block; wrong aggregations: 3 → 0; 3h.”
The SELECT * performance trap is the second category. A developer writes SELECT * FROM mara INTO TABLE lt_material WHERE mtart = lv_type. The MARA table has over 200 fields; the report uses 4 of them. On SAP ECC with a row-store database, the performance difference between SELECT * and a specific field list is modest: the database retrieves the full row regardless. On S/4HANA with HANA’s column-store architecture, SELECT * forces HANA to read every column from its column container and assemble a row-wide result; the column-store engine is optimized for column-at-a-time access, not row-wide retrieval, and cannot parallelize the fetch of 200 columns the way it handles a 4-column projection. The performance difference surfaces during S/4HANA migration testing. Work log entry: “Z_MAT_ANALYSIS: SELECT * from MARA, 180k rows, 200 columns retrieved, 4 used (matnr, maktx, mtart, meins); HANA column-store performance degraded by full-row retrieval; S/4HANA migration performance test revealed latency spike; rewritten with specific field list: SELECT matnr maktx mtart meins FROM mara INTO CORRESPONDING FIELDS OF TABLE lt_material WHERE mtart = lv_type; runtime: 45s → 2s; 2.5h.”
The missing BAPI_TRANSACTION_COMMIT is the third category. A developer integrates a custom batch program with SAP inventory management via BAPI_GOODSMVT_CREATE. The BAPI call completes; the RETURN table contains no error messages (all entries have TYPE = 'S' for success); the developer moves on. The next batch job reads SAP inventory and finds no change. The developer’s assumption: BAPIs are self-contained transactions that commit automatically. The reality: BAPIs operate within the SAP LUW (Logical Unit of Work), which is the SAP-level counterpart to a database transaction. BAPI_GOODSMVT_CREATE creates the goods movement document in the LUW buffer but does not close the LUW; the LUW is committed only when BAPI_TRANSACTION_COMMIT is called explicitly. Without the commit call, the LUW is discarded when the calling program ends, and no data reaches the database. Work log entry: “Z_WM_GOODS_MOVEMENT: BAPI_GOODSMVT_CREATE called for 340 movement records; RETURN table had no error entries; BAPI_TRANSACTION_COMMIT not called; goods movement documents created in SAP LUW but LUW discarded on program end; subsequent inventory reads from MARD showed no stock changes; fix: added CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING WAIT = 'X' after BAPI call; movements persisted: 0 → correct (340 documents); 2h.”
Track ABAP developer retainer hours without the status emails
When a 3-hour SAP ABAP debug session traces a 4-hour report timeout and 3 wrong production planning aggregations to a FOR ALL ENTRIES IN empty-table query returning 1.2 million rows — reproducible only at quarter-end when no open orders exist — the work log must name the internal table, the SAP Note number, the row count returned, and the aggregation count before and after. HourTab gives your ABAP retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the SAP-specific behavior and the guard condition. No client login. No status emails. CSV in, URL out.
How HourTab tracks ABAP developer retainer hours
ABAP retainer work is invisible by the same mechanism that makes FOR ALL ENTRIES IN empty-table behavior dangerous: the SAP kernel produces no warning and no error when the internal table is empty. The SELECT completes with SY-SUBRC = 0 — success — and returns a very large result set. Every previous run with a non-empty internal table was correct. The production failure occurs exactly once per quarter, for one to four hours, at the specific combination of time and business state that the developer’s test data never captured.
The work log needs to name the mechanism: which program, which SELECT, which internal table variable, what SAP Note governs the behavior, how many rows were returned on the empty-table path, and what the downstream impact count was before and after the guard condition. A log entry that says “fixed ABAP report performance issue, 3h” is not auditable. A log entry that says “Z_PP_REPLENISHMENT: FOR ALL ENTRIES IN lt_orders with lt_orders empty at quarter-end; SAP Note 71123 empty-table semantics; 1.2M rows from EBAN; 4h report timeout; 3 wrong aggregations; guard added IF lines(lt_orders) > 0; wrong aggregations: 3 → 0; 3h” is auditable.
HourTab gives ABAP developers a public retainer-hours URL they send to clients — manufacturing companies running production planning on SAP ECC, logistics firms maintaining warehouse management ABAP, and enterprises mid-migration from ECC to S/4HANA who need retainer support for ABAP compatibility remediation and HANA performance tuning. For ABAP retainers, each work log entry should name the ABAP artifact: which program or function module, which database table, what the ABAP-specific semantic was (not just “query returned too many rows” but the SAP Note and the internal table type), and what the symptom count changed from and to. Comparative context: ABAP retainer work shares the “invisible at test time, visible at production edge case” pattern with other enterprise language retainers — COBOL retainers cover OCCURS-clause table overflows and packed decimal truncation that surface only at specific field value combinations; Java retainers cover HashMap race conditions and JVM garbage collection pauses that appear only under production load profiles that test environments never replicate.
FAQ: ABAP developer retainers
What does an ABAP developer on retainer typically do?
An ABAP developer on monthly retainer covers FOR ALL ENTRIES IN empty-table diagnosis (identifying where an internal table passed to FOR ALL ENTRIES IN is empty at SELECT time, causing a full table scan; the fix is adding an IF lines(lt_table) > 0 guard); SELECT performance optimization (identifying SELECT * statements and SELECTs without WHERE clauses, and rewriting with specific field lists and appropriate conditions); BAPI and RFC integration debugging (identifying missing BAPI_TRANSACTION_COMMIT calls, RFC parameter mapping errors, and LUW boundary issues); ABAP OO exception handling (identifying swallowed exceptions, missing TEXTID attributes, and incorrect TRY/CATCH nesting); and S/4HANA migration compatibility (identifying ECC-era ABAP patterns incompatible with HANA column-store architecture or deprecated ABAP syntax).
What ABAP work is most commonly underlogged?
FOR ALL ENTRIES IN empty-table behavior is the most systematically underlogged ABAP retainer work. When the internal table passed to FOR ALL ENTRIES IN is empty, the SAP kernel ignores the clause and returns all rows from the target table. The SELECT completes with SY-SUBRC = 0 — no warning, no error, no diagnostic. Every previous run with a non-empty internal table was correct. The production failure occurs only at end-of-quarter or end-of-year when the business process produces zero selection records — a condition that may occur once per year. Diagnosis requires reading SAP Note 71123 and tracing the internal table’s population path through the report. Three to six hours invisible per occurrence.
What are typical ABAP developer retainer rates?
Entry-level ABAP developers with experience in ABAP SELECT statements, internal tables, and function modules typically bill at $80 to $145 per hour. Mid-level ABAP programmers with experience in ABAP OO, BAPI/RFC integration, S/4HANA CDS views, and ABAP Unit testing typically bill at $120 to $210 per hour. Senior ABAP developers with expertise in S/4HANA migration, HANA performance tuning, custom BAPI development, and the SAP enhancement framework typically bill at $170 to $310 per hour. Monthly retainer ranges: $2,000 to $3,800 per month for advisory engagements covering FOR ALL ENTRIES IN audits, SELECT reviews, and BAPI guidance (12 to 20 hours per month); $3,200 to $7,500 per month for active maintenance including S/4HANA migration support and HANA performance tuning.
What should an ABAP developer retainer agreement include?
An ABAP developer retainer agreement should specify: SAP system version (ECC 6.0, S/4HANA 2021/2022/2023 — determines available language features and database patterns); ABAP release version (7.02, 7.31, 7.40, 7.50, 7.57 — determines availability of inline declarations, string templates, and table expressions); CDS view scope (whether Core Data Services development and maintenance for S/4HANA reporting is included); ABAP OO scope (class library maintenance, exception class hierarchy, interface design); BAPI and RFC scope (custom BAPI development, RFC destination configuration, remote function module debugging); transport request process (whether the retainer covers transport management, release, and import coordination); ATC check scope (ABAP Test Cockpit static analysis findings and remediation); and HANA performance scope (EXPLAIN PLAN analysis, push-down optimization, and HANA-specific SELECT tuning).
How should ABAP developer retainer hours be logged?
Log each ABAP retainer session with the specific SAP artifact and ABAP-specific mechanism. For FOR ALL ENTRIES IN bugs: internal table variable name, target database table, row count returned on the empty-table path, SAP Note reference (SAP Note 71123), guard condition added (IF lines(lt_orders) > 0), and symptom count before and after (e.g., wrong aggregations: 3 → 0). For SELECT performance: field list change (SELECT * to specific fields), row count processed, runtime before and after (e.g., 45s → 2s). For BAPI commit issues: function module name (BAPI_GOODSMVT_CREATE), whether BAPI_TRANSACTION_COMMIT was added, and records persisted before and after (e.g., movements: 0 → 340). A log entry that says “fixed ABAP report, 2.5h” is not auditable. A log entry that names the program, the SELECT, the row count, and the runtime change is.