Hands on - Zeitscheiben organisieren in SAP BW

Für den Aufbau eines zentralen Core Data Warehouse bietet SAP BW viele Vorteile im Vergleich zu anderen Datenbanktechniken. Hier steht Datenqualität und korrekte fachliche Daten mehr im Vordergund als bei vergleichbaren Datenbanklösungen. So sind diverse Szenarien wie etwa Daten zurückrollen auf den Vorbeladungszustand beim Zurücknehmen einer ausgeführten Datenbeladung standardisiert.

Um stichtagsgerechte Berechnungen, speziell im Finance Bereich aber auch für machine learning/ AI Anwendungen zu gewährleisten, ist eine übergreifende funktionierende Historisierung in allen Datenbanktabellen über die gerechnet werden soll, maßgeblich. Eine funktionierende Historisierung besteht aus eindeutigen, lückenlosen und nicht überlappenden Zeitscheiben für jeden fachlichen Schlüssel. Reintechnische Zeitscheiben dienen eher der Nachvollzieharkeit von Korrekturen, sollten aber für fachlich richtige Ergebnisse nur mit neustem Stand verwendet werden. Daher betrachte ich im Folgenden nur die Erstellung einer richtigen Versionierung anhand von fachlichen "Gültig von" bis "Gültig bis" Zeiträumen.

Dies wird in der Regel über 2 weitere Schlüsselfelder im Datumsformat, welche den Gültigkeitsstart (GültigAb) und das Enddatum (GültigBis) festlegen erreicht. Deshalb muss für jeden neu erzeugten Datensatz die zugehörige alte Zeitscheibe geschlossen werden. Das Auslesen des richtigen Datensatz zum richtigen Zeitpunkt wird dann über die folgende Filterbedingung sichergestellt.

Exported from Notepad++
GültigAb <= Stichtag and GültigBis >= Stichtag

Da mir die Umsetzung einer Historisierung mittlerweile bei allen Kunden früher oder später begegnet, wollte ich mit diesem kleinen Hands on eine pragmatische Anleitung geben, wie die Historisierung nur einmal implementiert werden muss, um eine nachvollzieh- und pflegbare Lösung zu haben, aber dennoch noch weitestgehend dynamisch bleibt. Im Folgenden verwende ich die Info Objekte HDRVALTO und HDRVALFR. Wer eigene Info Objekte verwenden möchte, muss diese dann unten im Coding Beispiel noch anpassen. Ich habe dies als Public Static Methode (manage_hist) von überall aufrufbar implementiert. Als Input Parameter habe ich zum einen die Referenz auf das Result Package, der Ziel ADSO Name und Optional der Stichtag ab dem die neue Zeitscheibe generiert bzw. bis zu welchem Stichtag die alte Zeitscheibe nicht mehr gültig sein soll. In manchen Szenarien soll das Gültig von Datum aus den Daten heraus variabel generiert werden. Dies wird dann aus dem Result Package aus dem HDRVALR gelesen.

Exported from Notepad++
Class-methods MANAGE_HIST !it_result type Ref TO DATA !iv_adso_name type tabname16 !iv_date type dats OPTIONAL

Im ersten Teil werden die einzelnen Variablen deklariert, die Referenz des Result Package mit dem Feld Symbol <lt_result> verknüpft und je nach Optionaler Input Datum Befüllung die where Bedingungen definiert um aktuelle Zeitscheiben im Ziel zu ermitteln.

Exported from Notepad++
METHOD organize_history. DATA: lv_tab TYPE tabname, lr_hist_data TYPE REF TO data, lv_record TYPE int4, lv_keys TYPE string, lv_where TYPE string. FIELD-SYMBOLS: <lt_result> TYPE STANDARD TABLE, <lt_hist> TYPE STANDARD TABLE. ASSIGN it_result->* TO <lt_result>. lv_where = |/B20C/S_HDRVALTO = '99991231' |. IF iv_date IS NOT INITIAL. lv_where = lv_where && | and /B20C/S_HDRVALFR < '{ iv_date }' |. ELSE."Wenn DatumVon nicht gliefert wird, dynamisch aus Restult Package übernehmen lv_where = lv_where && | and /B20C/S_HDRVALFR < @<lt_result>-/B20C/S_HDRVALFR|. lv_keys = |( RECORDMODE = '' or RECORDMODE = 'N' ) and /B20C/S_HDRVALTO = '99991231'|. ENDIF.

Das dynamische Erzeugen des richtigen Tabellentypen, abhängig vom mitgegebenen iv_adso_name, wird dann über das Create Data Statemen, das eine Referenz vom Typ der aktiven ADSO Zieltabelle liefert vorgenommen. So erhält man die interne Tabelle vom Typ der aktiven Tabelle des Ziel ADSO durch Zusweisung der Referenz.

Exported from Notepad++
lv_tab = |/BIC/A| && iv_adso_name && |2|. CREATE DATA lr_hist_data TYPE STANDARD TABLE OF (lv_tab). ASSIGN lr_hist_data->* TO <lt_hist>.

Anhand der SAP DDICT Tabelle DD031 lassen sich sämtliche Schlüsselfelder einer Tabelle auslesen. Wir schreiben dann alle Schlüssel in die Where Bedingungen lv_where und lv_keys.

Exported from Notepad++
SELECT * FROM dd03l INTO TABLE @DATA(lt_fields) WHERE tabname = @lv_tab AND as4local = 'A' AND keyflag = 'X' AND fieldname NOT IN ('/B20C/S_HDRVALFR', '/B20C/S_HDRVALTO'). IF sy-subrc = 0. LOOP AT lt_fields ASSIGNING FIELD-SYMBOL(<ls_field>). lv_where = lv_where && | and { <ls_field>-fieldname } = @<lt_result>-{ <ls_field>-fieldname }|. lv_keys = lv_keys && | and { <ls_field>-fieldname } = <ls_hist>-{ <ls_field>-fieldname }|. ENDLOOP.

Mit der generierten Where Bedingung können wir dann direkt die zu schließenden Zeitscheiben ermitteln und weisen diesen der Standard Tabelle <lt_hist> zu.

Exported from Notepad++
SELECT * FROM (lv_tab) INTO TABLE @<lt_hist> FOR ALL ENTRIES IN @<lt_result> WHERE (lv_where).

Jetzt werden in einer Schleife über alle zu schließenden Zeitscheiben die existierende Zeitscheibe gelöscht. Dies geschieht über das Setzen von 'D' des Recordmodes und Hinzufügen zum Resultpackage. Anschließend müssen wir für dieselbe Zeitscheibe das Gültig Bis Datum HDRVALTO entweder auf Stichtag - 1 setzten. Bei Nichtbefüllung des Datums Inputvariable wird im Resultpackage nach dem vorhandenen passenden gültig Ab gesucht und dieses einen Tag vordatiert übernommen.

Exported from Notepad++
LOOP AT <lt_hist> ASSIGNING FIELD-SYMBOL(<ls_hist>). ASSIGN COMPONENT '/B20C/S_HDRVALTO' OF STRUCTURE <ls_hist> TO FIELD-SYMBOL(<val_to>). ASSIGN COMPONENT 'RECORDMODE' OF STRUCTURE <ls_hist> TO FIELD-SYMBOL(<recordmode>). "Löschung alter Datensatz <recordmode> = 'D'. APPEND INITIAL LINE TO <lt_result> ASSIGNING FIELD-SYMBOL(<ls_result>). MOVE-CORRESPONDING <ls_hist> TO <ls_result>. "Erstellen von geschlossener Zeitscheibe <recordmode> = 'N'. IF iv_date IS NOT INITIAL. <val_to> = iv_date - 1. ELSE. "Ende Datum individuell aus neuen Datensätzen ermitteln. LOOP AT <lt_result> ASSIGNING FIELD-SYMBOL(<lookup>) WHERE (lv_keys). ASSIGN COMPONENT '/B20C/S_HDRVALFR' OF STRUCTURE <lookup> TO FIELD-SYMBOL(<val_from_new>). <val_to> = <val_from_new> - 1. EXIT. ENDLOOP. ENDIF. APPEND INITIAL LINE TO <lt_result> ASSIGNING <ls_result>. MOVE-CORRESPONDING <ls_hist> TO <ls_result>. ENDLOOP.

Damit die Verbuchung und Aktivierung der Beladung im Ziel ADSO perfomanter ist sollte noch die Record Nummerierung neu gesetzt werden.

Exported from Notepad++
lv_record = 1. LOOP AT <lt_result> ASSIGNING <ls_result>. ASSIGN COMPONENT 'RECORD' OF STRUCTURE <ls_result> TO FIELD-SYMBOL(<record>). <record> = lv_record . ADD 1 TO lv_record. ENDLOOP.

Die Fehlerbehandlung wird über die Print Ausgabe der Fehlermeldung nur angedeutet. Hier sollte man sich eventuell mit einer eigenen Fehlerklasse in das vorhandene Fehlermanagement integrieren.

Exported from Notepad++
TRY. ... CATCH cx_root INTO DATA(lx_root). "ToDo: Eigenen Errorlog schreiben write: / 'Error Log: ', lx_root->get_text( ). ENDTRY.

Im folgenden stelle ich euch die gesamte Methode zum Wiederverwenden zur Verfügung.

Exported from Notepad++
"public static METHOD manage_hist. DATA: lv_tab TYPE tabname, lr_hist_data TYPE REF TO data, lv_record TYPE int4, lv_keys TYPE string, lv_where TYPE string. FIELD-SYMBOLS: <lt_result> TYPE STANDARD TABLE, <lt_hist> TYPE STANDARD TABLE. ASSIGN it_result->* TO <lt_result>. lv_where = |/B20C/S_HDRVALTO = '99991231' |. IF iv_date IS NOT INITIAL. lv_where = lv_where && | and /B20C/S_HDRVALFR < '{ iv_date }' |. ELSE. "Wenn DatumVon nicht gliefert wird, dynamisch aus Restult Package übernehmen lv_where = lv_where && | and /B20C/S_HDRVALFR < @<lt_result>-/B20C/S_HDRVALFR |. lv_keys = |( RECORDMODE = '' or RECORDMODE = 'N' ) and /B20C/S_HDRVALTO = '99991231'|. ENDIF. TRY. "Aktive Tabellen namen holen lv_tab = |/BIC/A| && iv_adso_name && |2|. CREATE DATA lr_hist_data TYPE STANDARD TABLE OF (lv_tab). ASSIGN lr_hist_data->* TO <lt_hist>. "Schlüsselfelder ermitteln. SELECT * FROM dd03l INTO TABLE @DATA(lt_fields) WHERE tabname = @lv_tab AND as4local = 'A' AND keyflag = 'X' AND fieldname NOT IN ('/B20C/S_HDRVALFR', '/B20C/S_HDRVALTO'). IF sy-subrc = 0. LOOP AT lt_fields ASSIGNING FIELD-SYMBOL(<ls_field>). lv_where = lv_where && | and { <ls_field>-fieldname } = @<lt_result>-{ <ls_field>-fieldname }|. lv_keys = lv_keys && | and { <ls_field>-fieldname } = <ls_hist>-{ <ls_field>-fieldname }|. ENDLOOP. "lesen der zu schließenden Zeitscheiben aus dem Ziel ADSO SELECT * FROM (lv_tab) INTO TABLE @<lt_hist> FOR ALL ENTRIES IN @<lt_result> WHERE (lv_where). IF <lt_hist> IS NOT INITIAL. "Schließen der Zeitscheiben LOOP AT <lt_hist> ASSIGNING FIELD-SYMBOL(<ls_hist>). ASSIGN COMPONENT '/B20C/S_HDRVALTO' OF STRUCTURE <ls_hist> TO FIELD-SYMBOL(<val_to>). ASSIGN COMPONENT 'RECORDMODE' OF STRUCTURE <ls_hist> TO FIELD-SYMBOL(<recordmode>). "Löschung alter Datensatz <recordmode> = 'D'. APPEND INITIAL LINE TO <lt_result> ASSIGNING FIELD-SYMBOL(<ls_result>). MOVE-CORRESPONDING <ls_hist> TO <ls_result>. "Erstellen von geschlossener Zeitscheibe <recordmode> = 'N'. IF iv_date IS NOT INITIAL. <val_to> = iv_date - 1. ELSE. "Ende Datum individuell aus neuen Datensätzen ermitteln. LOOP AT <lt_result> ASSIGNING FIELD-SYMBOL(<lookup>) WHERE (lv_keys). ASSIGN COMPONENT '/B20C/S_HDRVALFR' OF STRUCTURE <lookup> TO FIELD-SYMBOL(<val_from_new>). <val_to> = <val_from_new> - 1. EXIT. ENDLOOP. ENDIF. APPEND INITIAL LINE TO <lt_result> ASSIGNING <ls_result>. MOVE-CORRESPONDING <ls_hist> TO <ls_result>. ENDLOOP. "Record neu nummerieren lv_record = 1. LOOP AT <lt_result> ASSIGNING <ls_result>. ASSIGN COMPONENT 'RECORD' OF STRUCTURE <ls_result> TO FIELD-SYMBOL(<record>). <record> = lv_record . ADD 1 TO lv_record. ENDLOOP. ENDIF. ENDIF. CATCH cx_root INTO DATA(lx_root). "ToDo: Eigenen Errorlog schreiben write: / 'Error Log: ', lx_root->get_text( ). ENDTRY. ENDMETHOD.

Die Methode solltet ihr dann am Ende aller Experten-/ End-Routinen aufrufen.

Exported from Notepad++
CALL METHOD zcl_etl=>manage_history EXPORTING it_result = ref #( <result_package> ) iv_adso_name = 'ADSO_NAME'. " iv_date = BUSINESS_DATE "optional.