ABAP tips: Checking existence within an internal table
I got a blog-worthy surprise when I did a quick performance test on two different ways to use the new-ish (7.4) table expressions to perform a search on an internal table.
My scenario involved internal table searches where the values would be missing a lot of the time. The unexpected result was that when a row can’t be found, using table expressions with exceptions is ten times slower on average than all other lookup methods, including the good ol’ fashioned READ TABLE with SY-SUBRC.
Quick primer for those still getting accustomed to 7.4 ABAP (feel free to skip to the results):
With table expressions, finding an entry in a table is really neat. I have a table that includes a field “ID”. So the following READ TABLE can be rewritten using a table expression (the bit in square brackets):
READ TABLE itab INTO row WITH KEY id = find_id. "Equivalent to: row = itab[ id = find_id ].
If the matching entry cannot be found, you need to catch the exception:
TRY. row = itab[ id = find_id ]. CATCH cx_sy_itab_line_not_found. ... ENDTRY.
Another alternative is to use a VALUE constructor expression where you can add the keyword OPTIONAL (or DEFAULT) to just initialize any non-found values.
row = VALUE #( itab[ id = find_id ] OPTIONAL ). IF row IS INITIAL. ...
This particular usage of VALUE may seem a little awkward. I mean, why use x = VALUE #( y ) when you can just write x = y? The VALUE constructor’s sole purpose here is the OPTIONAL bit that lets us do away with the exception.
As I was working on a performance-sensitive component, I tested it to see what performs best.
- The trusty READ TABLE … WITH KEY, with and without TRANSPORTING NO FIELDS
- line_exists( )
- ASSIGN itab[ … ] – which (bizarrely) sets SY-SUBRC instead of producing an exception
- REF #( ) instead of VALUE #( ).
I tested a million failed lookups on a 7.50 HANA system. Here are the results in microseconds:
line_exists( ) : 610,758 READ TABLE ... TRANSPORTING NO FIELDS : 671,368 READ TABLE : 671,115 ASSIGN itab[ ... ] : 707,929 REF #( itab[ ... ] OPTIONAL ) : 888,562 VALUE #( itabl[ ... ] OPTIONAL ) : 961,803 TRY ... itab[ ... ] CATCH : 6,325,265
I did not expect the last one at all.
So the take-away here for me is that a TRY-CATCH may be easier to read, but should not be used in performance-sensitive code unless you expect the values to be found most of the time. I’m happy to sacrifice a little performance for readability, but this is a significant impact.
I suspect that this applies to CATCH blocks in general, but that’s another analysis for another day.
For comparison, I also re-ran the same but this time with a lookup value that did exist. Two things happened:
- the exception was no longer an influence (to be expected)
- line_exists( ) became a poor choice in my scenario, because we need to double up the the search in order to also read the record:
IF line_exists( itab[ id = find_id ] ). row = itab[ id = find_id ].
- If you don’t need the data, line_exists( ) is fastest.
- If performance is number 1 priority and you need the data, READ TABLE is fastest.
- For compact and/or readable code, use table expressions.
(Yes, I know, ‘new’ ABAP can be used to make code either more readable or more cryptic, but that’s another discussion)
- TRY-CATCH with table expressions can be a useful way to structure your code (e.g. to use a single catch handler for multiple expressions), but be mindful of the expected failure rate and performance-criticality of the component. If we’re talking anything less than thousands in a short space of time then you can safely ignore the impact.
After several comments and suggestions, I uploaded my test program to GitHub at abap_itab_perf
It was just a quick-n-dirty originally for comparing just two options and just grew as a copy/paste of alternatives. Feel free to play with it or even send any updates back via GitHub.