ABAP - SELECT FROM @itab
Über eine interne Tabelle selektieren war früher mit vielen Zeilen Code realisiert, heute funktioniert es auch praktisch über den Select.
Inhaltsverzeichnis
In diesem Artikel wollen wir uns einmal die Erweiterung des SELECT Stamenet für lokale Tabellen anschauen und wofür wir es sinnvollerweise nutzen können. Im Nachgang wollen wir uns auch einmal die Performance beim Zugriff auf die Daten anschauen.
Vorbereitung
Bevor wir auf Daten zugreifen können, sollten wir uns zuerst einmal ein paar Dummy Daten generieren. Diese Daten kommen an dieser Stelle nicht aus der Datenbank, sondern wir erzeugen diese aus einer eigenen Struktur und einer eigenen kleinen Logik.
DATA(lo_random_number) = NEW zcl_demo_random( id_min = 1 id_max = 99 ).
DATA(lo_random_currency) = NEW zcl_demo_random( id_min = 1 id_max = 3 ).
DO id_number_of_entries TIMES.
TRY.
DATA(ls_structure) = VALUE ts_structure(
guid = cl_system_uuid=>create_uuid_x16_static( )
number = lo_random_number->rand( )
text = |Item No. { sy-index }|
currency = SWITCH #( lo_random_currency->rand( )
WHEN 1 THEN 'EUR'
WHEN 2 THEN 'USD'
WHEN 3 THEN 'CHF'
)
).
CATCH cx_uuid_error.
CONTINUE.
ENDTRY.
INSERT ls_structure INTO TABLE rt_result.
ENDDO.
DATA(lo_random_index) = NEW zcl_demo_random( id_min = 1 id_max = id_number_of_entries ).
DATA(ld_done) = 0.
WHILE ld_done < id_number_of_gbp.
DATA(ld_pos) = lo_random_index->rand( ).
IF rt_result[ ld_pos ]-currency <> 'GBP'.
rt_result[ ld_pos ]-currency = 'GBP'.
ld_done += 1.
ENDIF.
ENDWHILE.
Es wird eine interne Tabelle mit zufälligen Daten erzeugt. In der ersten Schleife wir die Tabelle mit zufälligen Werten, Währungen und einem fortlaufenden Text befüllt. In der zweiten Schleife ändern wir eine gewisse Anzahl Zeilen und tauschen die Währung aus.
Standard Zugriff
Nun können wir gegen die erzeugte interne Tabelle Zugriff per SELECT durchführen, so als würden wir über eine Datenbank Tabelle lesen, dabei benötigen wir aber noch zusätzlich einen Alias für die Tabelle. Hier einige Beispiele:
" Normal Select with currency
SELECT *
FROM @lt_standard_data AS data
WHERE currency = 'GBP'
INTO TABLE @DATA(lt_currency_gbp).
out->write( lt_currency_gbp ).
" Select with fields
SELECT text, number
FROM @lt_standard_data AS data
WHERE currency = 'GBP'
AND number > 50
INTO TABLE @DATA(lt_fields_gbp).
out->write( lt_fields_gbp ).
" Count
SELECT COUNT(*)
FROM @lt_standard_data AS data
WHERE number < 10
INTO @DATA(ld_low_count).
out->write( ld_low_count ).
Im ersten Zugriff lesen wir alle Felder und schränken auf eine Währung ein, das Ergebnis sind die entsprechenden Zeilen aus der Tabelle. Im zweiten Zugriff wollen wir nur zwei Felder lesen und haben eine erweitere Abfrage gegen zwei Felder. Und wie du es vom SELECT kennst, kannst du per Inline-Deklaration auch eine neue interne Tabelle mit eigenem Zeilentyp generieren. In der dritten Abfrage wollen wir Datensätze zählen lassen, was ebenfalls funktioniert und uns die Anzahl Datensätze mit einer Nummer kleiner als 10 zurück gibt.
Erweiterte Zugriffe
Schauen wir uns nun einen erweiterten Fall an der etwas komplexer ist, weil wir hier noch zusätzlich mit einer Sortierung der Daten arbeiten.
SELECT *
FROM @lt_standard_data AS data
WHERE number > 20 AND number < 30
ORDER BY number DESCENDING
INTO TABLE @DATA(lt_order)
UP TO 20 ROWS.
out->write( lines( lt_order ) ).
out->write( lt_order ).
Wir schränken die Daten ein, sortieren das Ergebnis und wollen davon die ersten 20 Zeilen lesen. In diesem Fall bekommen wir eine entsprechende Meldung des Compilers zu lesen.
Was ist also passiert? Je nach Komplexität und Zugriffsform auf die Tabelle, entscheidet das System ob die Anfrage auf dem ABAP Stack durchgeführt wird oder eine temporäre Tabelle erzeugt wird, um die Operationen darauf auszuführen. Der Compiler weißt auf diese Form hin, dass die Anfrage nicht auf dem ABAP Stack durchgeführt werden kann.
Hinweis: Mehr zu diesem Verhalten kannst du in der offiziellen Dokumentation lesen (siehe unten).
Performance
Wie sieht es nun mit der Performance beim Zugriff auf die Daten über die verschiedenen Formen aus? Dazu nehmen wir die Tabelle mit 2 Mio. Datensätzen und führen den SELECT auf die Standard Tabelle und die Sortierte Tabelle mit Sekundärschlüssel aus. Zusätzlich nehmen wir für diesen Fall noch das neue FILTER Statement, um an die passenden Daten zu gelangen.
SELECT *
FROM @lt_standard_data AS data
WHERE currency = 'GBP'
INTO TABLE @DATA(lt_standard_gbp).
SELECT *
FROM @lt_sorted_data AS data
WHERE currency = 'GBP'
INTO TABLE @DATA(lt_sorted_gbp).
DATA(lt_filtered_gbp) = FILTER #( lt_sorted_data USING KEY curr_key WHERE currency = 'GBP ' ).
Entsprechend benötigen wir auch noch eine Zeitmessung, um den Vergleich durchführen zu können. Das vollständige Beispiel findest du unten. So erhalten wir nun die entsprechende Zeitmessung:
Am Beispiel können wir gut nachvollziehen, dass die Nutzung des SELECTs nicht ohne Performanceverlust funktioniert. Der Zugriff auf die sortierte Tabelle dauert über eine halbe Sekunde. Der Zugriff über FILTER benötigt kaum Performance. Auffällig ist auch noch, dass der Zugriff auf die Standardtabelle schneller ist, als auf die sortierten Daten. Dieses Verhalten können wir nicht genau erklären, denken aber es hängt an der Persistierung der Daten und der Verwaltung des sekundären Schlüssels.
Beispiel
Wie immer findest du auch am Ende des Artikels das vollständige Beispiel welches wir genutzt haben. Mehr Infos zum Zufallsgenerator findest du in einem anderen Artikel.
CLASS zcl_select_with_itab DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
TYPES:
BEGIN OF ts_structure,
guid TYPE sysuuid_x16,
number TYPE i,
text TYPE c LENGTH 40,
currency TYPE waers,
END OF ts_structure,
tt_standard TYPE STANDARD TABLE OF ts_structure WITH EMPTY KEY,
tt_sorted TYPE SORTED TABLE OF ts_structure
WITH UNIQUE KEY guid
WITH NON-UNIQUE SORTED KEY curr_key COMPONENTS currency.
PROTECTED SECTION.
PRIVATE SECTION.
METHODS:
get_initial_data
IMPORTING
id_number_of_entries TYPE i DEFAULT 1000
id_number_of_gbp TYPE i DEFAULT 5
RETURNING VALUE(rt_result) TYPE tt_standard.
ENDCLASS.
CLASS zcl_tselect_with_itab IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA(lt_standard_data) = get_initial_data( id_number_of_entries = 2000000 ).
DATA(lt_sorted_data) = CORRESPONDING tt_sorted( lt_standard_data ).
" Normal Select with currency
SELECT *
FROM @lt_standard_data AS data
WHERE currency = 'GBP'
INTO TABLE @DATA(lt_currency_gbp).
out->write( lt_currency_gbp ).
" Select with fields
SELECT text, number
FROM @lt_standard_data AS data
WHERE currency = 'GBP'
AND number > 50
INTO TABLE @DATA(lt_fields_gbp).
out->write( lt_fields_gbp ).
" Count
SELECT COUNT(*)
FROM @lt_standard_data AS data
WHERE number < 10
INTO @DATA(ld_low_count).
out->write( ld_low_count ).
" Select with order and limitation
SELECT *
FROM @lt_standard_data AS data
WHERE number > 20 AND number < 30
ORDER BY number DESCENDING
INTO TABLE @DATA(lt_order)
UP TO 20 ROWS.
out->write( lines( lt_order ) ).
out->write( lt_order ).
" Performance check
DATA(lo_timer) = NEW zcl_demo_runtime( ).
DATA(ld_start) = lo_timer->get_runtime( ).
SELECT *
FROM @lt_standard_data AS data
WHERE currency = 'GBP'
INTO TABLE @DATA(lt_standard_gbp).
out->write( |Standard: { lo_timer->get_runtime( ) - ld_start }| ).
ld_start = lo_timer->get_runtime( ).
SELECT *
FROM @lt_sorted_data AS data
WHERE currency = 'GBP'
INTO TABLE @DATA(lt_sorted_gbp).
out->write( |Sorted: { lo_timer->get_runtime( ) - ld_start }| ).
ld_start = lo_timer->get_runtime( ).
DATA(lt_filtered_gbp) = FILTER #( lt_sorted_data USING KEY curr_key WHERE currency = 'GBP ' ).
out->write( |Filter: { lo_timer->get_runtime( ) - ld_start }| ).
ENDMETHOD.
METHOD get_initial_data.
DATA(lo_random_number) = NEW zcl_demo_random( id_min = 1 id_max = 99 ).
DATA(lo_random_currency) = NEW zcl_demo_random( id_min = 1 id_max = 3 ).
DO id_number_of_entries TIMES.
TRY.
DATA(ls_structure) = VALUE ts_structure(
guid = cl_system_uuid=>create_uuid_x16_static( )
number = lo_random_number->rand( )
text = |Item No. { sy-index }|
currency = SWITCH #( lo_random_currency->rand( )
WHEN 1 THEN 'EUR'
WHEN 2 THEN 'USD'
WHEN 3 THEN 'CHF'
)
).
CATCH cx_uuid_error.
CONTINUE.
ENDTRY.
INSERT ls_structure INTO TABLE rt_result.
ENDDO.
DATA(lo_random_index) = NEW zcl_demo_random( id_min = 1 id_max = id_number_of_entries ).
DATA(ld_done) = 0.
WHILE ld_done < id_number_of_gbp.
DATA(ld_pos) = lo_random_index->rand( ).
IF rt_result[ ld_pos ]-currency <> 'GBP'.
rt_result[ ld_pos ]-currency = 'GBP'.
ld_done += 1.
ENDIF.
ENDWHILE.
ENDMETHOD.
ENDCLASS.
Fazit
Benötigt man überhaupt noch READ TABLE wenn es nun auch SELECT gibt? DIe Antwort ist nicht leicht, vor allem wenn man auf die Performance der Anweisung schaut und bereits Funktionen existieren die es so ähnlich können. In speziellen und komplexen Fällen lohnt es sich aber über das SELECT Statment zu gehen, um so Code schlanker zu machen.