AMDP Coding Guidelines

AMDP Coding Guidelines

Veröffentlicht am 12. Juli 2022 von

Jörg Brandeis

| SQLScript | AMDP | BW/4HANA |

Diese AMDP Programmierrichtlinien sind ein Vorschlag für ABAP und SAP BW Projekte. Sie sind ursprünglich für den Anwendungsfall von AMDP Transformationsroutinen im SAP BW/4HANA geschrieben worden. Sie können sie gerne kopieren, anpassen und in Ihren eigenen Projekten nutzen, solange dieser Artikel als Quelle verlinkt ist.

Call by Reference - Ein Hinweis zur Nutzung

Dieses Dokument wird im Laufe der Zeit stets aktualisiert: Es wird an neue technische Entwicklungen angepasst, Fehler werden korrigiert und Fehlendes wird ergänzt. Darum empfehle ich Ihnen, dieses Dokument nicht zu kopieren sondern zu verlinken. Damit haben Sie stets den neusten Stand. Ein Interessanter Artikel zu diesem Thema hat Jelena Perfiljeva geschrieben: Are Your ABAP Guidelines Misguided?


Vorwort

Entwicklungsrichtlinien haben mehrere Ziele. Unter anderem:

  • Gute Les- und Wartbarkeit des Quellcode
  • Einheitlicher Quellcode
  • Vermeidung von Fehlerquellen
  • Vermeidung von langer Laufzeit

Diese Guidelines ersetzen nicht eine fundierte Ausbildung der Entwickler. Im Umkehrschluss enthalten sie nicht alle möglichen Fallstricke, über die man stolpern kann. Und auch unter strengster Beachtung aller Richtlinien können Fehler und unsauberer Code geschrieben werden. Jeder Entwickler ist dafür veranwortlich, dass sein Quellcode über diese Richtlinien hinaus professionellen Ansprüchen genügt.

Unsauberer Code ist nicht fertig

Der Sinn dieser Guidelines ist sind Minimalanforderungen an die Qualität und Einheitlichkeit. Sie sollten als Teil der Definition of Done für AMDP Transformationsroutinen betrachtet werden. Das bedeutet: Wenn diese Guidelines (noch) nicht vollständig umgesetzt sind, dann ist es der Code auch nicht fertig und darf nicht transportiert werden. Dies gilt auch dann, wenn der Code schon das gewünschte Ergebnis liefert.

Ausnahmen

Natürlich gibt es Situationen, in denen man aus technischen Gründen von den Guidelines abweichen muss. Das sollte man aber

  1. Nicht alleine entscheiden, sondern immer noch einen kompetenten Kollegen mit einbeziehen. Häufig fehlen einem einfach die guten Ideen.
  2. Auch explizit im Code in einem Kommentar hinschreiben, warum das so gemacht wurde.

Lesbarkeit des Codes

In sauberem Code können sich keine Fehler verstecken. - Sie können zwar vorkommen, sind dann aber schnell zu entdecken. Entsprechend gilt:

Lesbarkeit vor Optimierung - premature optimization is the root of all evil

There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. (Donald E. Knuth, 1974 ! )

Gut strukturierter und gegliederter SQLScript Quellcode, der die im Folgenden beschriebenen Punkte beachtet, kann normalerweise gut optimiert und schnell von der Datenbank ausgeführt werden. Vorauseilende Optimierung, die über die unten genannten Punkte hinaus geht, wird nicht vorgenommen. Bevor Performanceoptimierungen vorgenommen werden, muss auch tatsächlich ein Probleme vorliegen und eine gründliche Laufzeitanalyse durchgeführt worden sein.

Namensgebung

Einige wichtige Punkte sind dazu im Folgenden aufgelistet. Eine umfangreichere und ausführlichere Diskussion des Themas mit vielen Beispielen und guten Gründen finden Sie im Buch Clean Code von Robert C. Martin.

  • Alle Bezeichner im Code oder in der Datenbank sind in englischer Sprache.
  • Die Namen sollen aussagekräftig sein und den Inhalt beschreiben.
  • Tabellen und Tabellenvariablen enden mit einem Plural-S. Beispiel: Materials oder SalesOrderItems
  • Nummerierungen oder Allgemeinplätze sind zu vermeiden.
  • Für jedes Konzept soll exakt ein Name festgelegt sein. Negativbeispiel: Die Begriffe Article, Product und Material werden für das Gleiche verwendet.

Die Notation von Bezeichner

  • Die spezielle Notation mit Gänsefüsschen soll vermieden werden. Sie wird nur dort verwendet, wo die einfache Notation nicht verwendet werden kann. Insbesondere bei den Namensraumpräfixen /BIC/ und /BI0/ ist das der Fall.
  • Alle Bezeichner in der einfachen Notation (=ohne Gänsefüsschen) werden intern in Großbuchstaben umgewandelt. Also kann man die Groß- /Kleinschreibung für die bessere Lesbarkeit von Wörtern verwenden, z.B. SalesOrderItems liest sich besser als salesorderitems.

Ungarische Notation und Präfixes

Im SQLScript hat die ungarische Notation keinen Vorteil. Die Unterscheidung zwischen Tabellenvariablen und skalaren Variablen erfolgt eindeutig anhand der Position im Quelltext. Die Abgrenzung zwischen Tabellenvariablen und DB-Tabellen ist ebenso eindeutig.

Einrückung und Formattierung

Die Lesbarkeit des Quellcodes ist für eine gute Wartbarkeit und zur Identifikation von Fehlern sehr wichtig. Einrückungen mit Whitespace erhöhen diese enorm. Die folgenden Regeln sind grundsätzlich anzuwenden:

  • Für die Feldliste
    • Jedes in der Feldliste ist in einer eigenen Zeile
    • Die Felder sind sauber in einer Linie ausgerichtet
    • Bei verschachtelten SQL-Funktionen werden die Parameter
      • jeder in einer eigenen Zeile geschrieben
      • untereinander eingerückt
  • Vor den Klauseln und Logischen Operatoren (FROM, WHERE, GROUP BY, ORDER BY, ON, AND, OR etc. ) kommt ein Zeilenumbruch.
  • Zwischen Anweisungen ist stets eine Leerzeile einzufügen
  • Blöcke werden eingerückt

Beispiel

Sauber formatiert

CONCAT( 
        CONCAT( 
                LEFT( 
                     ABAP_UPPER( firstname)
                    , 1) 
              , '. ')
      , INITCAP( lastname )) AS Name,

Ohne Zeilenumbrüche und Einrückung

CONCAT( CONCAT( LEFT( ABAP_UPPER(  firstname)
 , 1) , '. ') , INITCAP( lastname )) AS Name,

Alias Namen und Korrelationsnamen

Für die Vergaben von Alias Namen von Spalten und für die Umbenennung von Tabellenausdrücken mit Korrelationsnamen ist das Schlüsselwort AS Pflicht. Es erhöht die Lesbarkeit erheblich.

  • Spalten-Aliasse sollen nur verwendet werden, wenn sie notwendig sind.
  • Korrelationsnamen sollen immer vergeben werden, wenn mehr als eine Tabelle involviert ist. D.h. bei JOINs oder bei korrellierten Unterabfragen.

Schlüsselwörter vollständig hinschreiben

Schlüsselwörter sind immer in der vollständigen Form hinzuschreiben, auch wenn sie optional sind oder eine Kurform existiert. Beispiele:

  • CROSS JOIN statt ,
  • AS
  • INNER JOIN statt JOIN
  • LEFT OUTER JOIN statt LEFT JOIN

Lange Feldlisten vermeiden

Wenn man in jedem SELECT alle Felder hinschreibt, dann wird der Quellcode episch lang. Statt dessen nehmen wir immer alle Felder aus dem vorherigen Schritt mit Stern * und fügen die neuen Spalten hinzu.

intab_with_colors = SELECT it.*,
                           IFNULL(ma.color, '') AS color
                      FROM :intab as it 
                      LEFT OUTER JOIN mara AS ma
                      ON ma.matnr = it.matnr;

Warnung: Das gilt nicht im ABAP. Hier sollte ein SELECT * vermieden werden.

Klammerung

Sobald in Ausdrücken mehr als ein Operator im Spiel ist, müssen Klammern gesetzt werden. Das kostet keine Laufzeit erhöht die Lesbarkeit erheblich. Beispiel:

Was hat der Entwickler hier vor gehabt?

WHERE doctype = 'Z001'
  AND itemcat = 'A001' 
   OR itemcat = 'B001' 

So hat es die Datenbank verstanden

WHERE ( doctype = 'Z001'
    AND itemcat = 'A001' )
     OR itemcat = 'B001' 

Und das wollte der Entwickler erreichen

WHERE   doctype = 'Z001'
  AND ( itemcat = 'A001' 
     OR itemcat = 'B001 )' 

Kommentare

Grundsätzlich soll in produktivem Code immer der Zeilenendkommentar, eingeleitet durch den doppelten Bindestrich -- verwendet werden. Blockkommentare mit /*...*/ hingegen sind dem Arbeiten in der SQL-Konsole vorbehalten. ABAP Zeilenkommentare (der Stern * am Zeilenanfang) sind verboten, auch wenn sie in AMDP funktionieren. Der Grund ist, dass sich der Code nicht mehr 1:1 in der SQL-Konsole ausführen lässt.

  • Einleitungskommentare beschreiben die Struktur der Prozedur als Ganzes und in welchem Kontext sie steht.
  • Gliederungskommentare erleichtern die Navigation im Code. Sie unterteilen diesen in Abschnitte.
  • Erklärende Kommentare beschreiben, warum etwas gemacht wurde oder erläutern komplexe Logik, beispielsweise in regulären Ausdrücken. Sie unterstützen den Lesefluss.
  • Kommentare, die das offensichtliche beschreiben, sind überflüssig.
  • Kopfkommentare die die Informationen aus dem Objektkatalogeintrag und der Versionshistorie wiederholen sind ebenfalls überflüssig.

Auskommentierter Code ist nur während der Entwicklungsphase akzeptabel und hat in produktivem Code nichts zu suchen.

Dos and Don'ts

Die HANA Datenbank ist dafür optimiert, dass sie deklarativen Code ausführt. Imperativer Code läuft erheblich langsamer. In erster Linie, weil eine Parallelisierung verhindert wird. Darüber hinaus verhinder Imperativer Code eine vollständige Optimierung. Deshalb gilt:

Imperativer Code ist verboten

Ausnahmen bedürfen einer gründlichen Prüfung.

Damit bleiben nur wenigen Anweisungen übrig, die im AMDP verwendet werde dürfen:

  • SELECT Anweisungen
  • Zuweisung von Tabellenvariablen
  • Aufruf von deklarativen Prozeduren

Dynamisches SQL ist zu vermeiden

Dynamisches SQL lässt sich nicht gut von der Datenbank optimieren. Je nach Anwendungsfall gibt es auch gute Alternativen:

Darüber hinaus ist die Komplexität von dynamischem Code stets erheblich höher und es fällt schwer ihn nachzuvollziehen. Ausserdem ermöglicht er Code Injection Angriffe.

Abfragen in kleine Schritte aufteilen

Komplexität in einer SELECT-Abfrage entsteht immer dann, wenn mehr als eine Sache gleichzeitig gemacht wird, beispielsweise JOIN und Aggregation oder der mehrere JOIN-Operationen auf einmal. Das erschwert die Lesbarkeit und die Fehlersuche. Statt dessen sollen die Abfragen in mehrere kleine Schritte, die durch Tabellenvariablen verbunden sind, aufgeteilt werden.

Beispiel

Ein Schritt

Die Mischung von JOIN und GROUP BY erschwert die Lesbarkeit. Der kürzere Code täuscht Einfacheit vor.

DO BEGIN

	SELECT u.firstname, 
	       u.lastname,
	       COUNT(*) AS task_cnt,
	       MAX(due_date) AS max_dd
	    FROM users AS u
	  INNER JOIN tasks AS t
	  ON u.id = t.assignee
	  GROUP BY u.firstname, 
	           u.lastname;
            
END;

Zwei Schritte

Die Zerlegung in zwei triviale Schritte erhöht die Lesbarkeit. Sinnvollen Namen für die Tabellenvariablen unterstützen dabei.

DO BEGIN

	tasks_per_user = SELECT u.firstname, 
	                        u.lastname,
	                        t.id,
	                        t.due_date
	                    FROM users AS u
	                  INNER JOIN tasks AS t
	                  ON u.id = t.assignee;
	
	SELECT firstname, 
	       lastname,
	       COUNT(id) AS task_cnt,
	       MAX(due_date) AS max_dd 
	  FROM :tasks_per_user
	 GROUP BY firstname, 
	          lastname; 

END;           

Unterabfragen

Unterabfragen sind nur in der WHERE Klausel erlaubt. Insbesondere in den Prädikaten EXISTS und IN, so wie als Vergleichswert Vergleichsprädikaten.

In der FROM-Klausel sind Unterabfrfagen verboten. Statt dessen sind Tabellenvariaben zu nutzen. Grund hierfür ist die geringere Komplexität und bessere Lesbarkeit.

In der Feldliste sind skalare Unterabfragen ebenfalls nicht erlaubt. Statt dessen müssen die Daten per JOIN hinzugefügt werden. Gegebenenfalls müssen diese vorab in einer Tabellenvariablen aufbereitet werden. Grund hierfür ist die Performance.

NULL-Werte sofort abfangen

Der Code ist so zu gestalten, dass keie NULL-Werte in Tabellenvariablen stehen. In den SAP BW Tabellen können wir uns darauf verlassen, dass NULL nicht in der Datenbank steht. Trotzdem kann NULL innerhalb unserer Routinen entstehen, z.B. durch OUTER JOIN oder CASE Ausdrücke. Mit den Funktionen

  • IFNULL() und
  • COALESCE() wird NULL durch einen geeigneten Wert ersetzt.

Mit dem Prädikat IS NULL bzw. IS NOT NULL können Datensätze mit NULL-Wert herausgefiltert werden.

Alle CASE Ausdrücke müssen einen ELSE-Zweig haben um den NULL-Wert zu verhindern.

Session Variablen

Die vordefinierten Session Variablen dürfen nur lesend verwendet werden. Eine Veränderung kann die Verarbeitung an anderen Stellen der Programmlogik beeinflussen.

Selbst definierte Session Variablen sollen nicht verwendet werden.

Implizites Casting vermeiden

Die HANA Datenbank führt implizit Typkonvertierungen aus, wo das notwendig ist. Das erschwert die Lesbarkeit und erhöht die Fehleranfälligkeit. Darum soll der Entwickler immer explizite Konvertierungen verwenden und wo möglich bei der Konvertierung der korrekte Typ verwendet werden.

Fehlende Typkonvertierungen können Anfänger auch auf die falsche Fährte locken

Beispiel falsche Rückschlüsse aus fehlender Typkonvertierung

SELECT LEFT(current_date, 4) AS year  --liefert das aktuelle Jahr, fast wie in ABAP
  FROM dummy;

Die Zeichenkettenfunktion LEFT macht einen glauben, dass das Datum intern eine Zeichenkette ist. Mit Explain können wir aber herausfinden, dass die Datenbank vorher einen TO_CHAR macht. Und falls ein Spaßvogel vorher das Default Format von DATE geändert hat, dann liefert die obigen Anweisung vielleicht '12/0'.

Typgerechte Literale verwenden

Konstante Werte werden als Literal im Quellcode geschrieben. Diese sollten immer vom richtigen Datentyp sein. Die wichtigsten Literale:

  • Zeichenketten werden in Hochkomma geschrieben: 'Zeichenkette'
  • Unicode-Zeichenketten mit führendem, großen N: N'Jörg'
  • Numerische Daten werden ohne Hochmkoma direkt hingeschrieben. Dezimaltrennzeichen ist der Punkt. 3.1415
  • Zeit-Literale werden als Zeichenkette im Standardformat mit vorangestelltem Datentyp geschrieben: DATE'2022-07-12'

Beispiel mehrdeutiger Code wegen unterschiedlichem Datentyp

Anmerkung: Die HANA Datenbank führt das nach eindeutigen Regeln aus, mehrdeutig ist das nur für den menschlichen Leser.

WHERE datum < '20090119';

Da SQL die Datentypen DATE und VARCHAR nicht vergleichen kann, findet zunächst eine Konvertierung statt. Aber welche?

  • das Datum in eine Zeichenkette ('2009-01-19') oder
  • die Zeichenkette in ein Datum

So ist es eindeutig:

WHERE datum < date'2009-01-19'; 

Damit wird der Vergleich leicht erkennbar zwischen zwei DATE Feldern durchgeführt.

Literale mit einem Leerzeichen

Auf Grund einer Besonderheit im AMDP verhalten sich Literale mit nur genau einem Leerzeichen anders als erwartet, siehe dieser Blog Artikel Der ABAPVARCHARMODE: Leerzeichen und leere Zeichenketten in ABAP und SQLScript. Statt dessen ist die SQL-Funktion CHAR(32) zu verwenden:

  SELECT firstname || CHAR(32) || lastname AS Name
    FROM users; 

Datenvolumen früh einschränken

Alle statisch bekannten Filterbedingungen werden direkt beim Datenbankzugriff angewendet.

Skalare UDFs vermeiden

Skalare User Defined Functions (UDF) sind potenziell langsam in der Ausführung, siehe dieser Blog Artikel. Darum sollten sie nicht verwendet werden.

Keine Konvertierungen und SQL-Funktionen in JOIN-Bedingungen

Ein Equi-Join wird von der Datenbank am schnellsten verarbeitet. Jede Konvertierung verringert die Ausführungsgeschwindigkeit. Ungünstig sind

  • Unterschidliche Datenlängen
  • Unterschiedliche Datentypen, die Explizit oder Implizit konvertiert werden müssen
  • SQL-Funktionen wie z.B. LPAD() oder LTRIM()

In diesen Fällen soll geprüft werden, ob die entsprechende Spalte in einem für den JOIN optimierten Format persistiert werden kann.

UNION ALL bevorzugen

Falls möglich ist die Mengenoperation UNION zu vermeiden und statt dessen UNION ALL zu verwenden. Ersterer entfernt Duplikate aus der Ergebnismenge. Das ist häufig überflüssig, da

  • Keine Duplikate in den Daten vorkommen, z.B. weil ein Feld RECORD oder TIMESTAMP stets unterschidlich ist oder weil die Daten entsprechend selektiert wurden
  • Es egal ist, ob in der Ergebnismenge Duplikate vorkommen, z.B. in der OUTTAB einer Transformationsroutine. Hier überschreiben sich die Duplikate beim Aktivieren der Daten.

Window Functions vorsichtig einsetzen

Window Functions (WF) verschlechtern die Performance aus mehreren Gründen:

  • Die Optimierung wird durch WF teilweise blockiert. z.B. können Filterbedingungen nicht nach unten verschoben werden.
  • Die WF werden in der Row-Engine ausgeführt. Die Daten müssen also zwischen den Engines hin und her kopiert werden.

Für viele Punkte sind Window Functions aber die beste bzw. einzige Lösung. Hier muss man sicherstellen, dass die Anzahl der Datensätze, auf die sie angewendet wird möglichst klein ist. Also alle Filterungen vor der WF stattfinden.

Falls die WF auf eine sehr große Datenbanktabelle bezieht, kann in Transformationsroutinen ein voriger INNER JOIN mit der INTAB das Datenvolumen wirkungsvoll einschränken.

Datenbankhints

Datenbankhints werden nicht verwendet.

Falls die SAP einen Hint als Reaktion auf eine Meldung empfliehlt, muss nach jedem Update geprüft werden, ob dieser noch notwendig bzw. hilfreich ist.

Speziell für AMDP Transformationsroutinen

Die folgenden Themen beziehen sich nur auf AMDP Transformationsroutinen im BW/4HANA oder BW on HANA. Sie haben keine Relevanz für eine allgemeine AMDP und SQLScript Entwicklung.

Maximal eine Routine

Die Logik in einer Transformation sollte möglichst zentral an einer Stelle implementiert werden. Für eine bessere Übersicht soll eine Verteilung auf viele Baustellen vermieden werden. Das bezieht sich nicht nur auf Routinen sondern auch auf Formeln.

Verwendung von DDic-Based CDS Views

Die neuen CDS View Entities (ab ABAP release 7.55) haben ein großen Nachteil in AMDP Routinen. Sie können dort zum Lesen von Daten eingesetzt werden. Aber es ist nicht möglich, den Quellcode dann in die SQL-Konsole zu kopiere und dort auszuführen.

Deswegen soll beim Datenbankzugriff per CDS immer der SQL-View der alten DDic-Basierten CDS Views verwendet werden.

Struktur der Routinen

AMDP Transformationsroutinen werden sinnvoller Weise in mehrere Schritte untergliedert. Bewährt hat sich die folgende Struktur:

  1. Eingangsprojektionen - Alle in der USING Klausel genannten Tabellen werden ganz am Anfang in eine Tabellenvariable geladen. Dabei gilt:
    • Nur die Spalten mitnehmen, die später gebraucht werden
    • Notwendige Berechnungen können hier gemacht werden
    • Umbenennung von Spalten kann sinnvoll sein, damit später mit einem Stern in der Feldliste gearbeitet werden kann.
    • Die Daten werden, soweit möglich, bereits eingeschränkt
  2. Viele kleine Schritte - In jedem Schritt immer nur einen Aspekt bearbeiten. Die Daten aus dem vorigen Schritt können mit * mitgenommen werden.
  3. Ausgangsprojektion - Ganz am Ende kommt eine Ausgangsprojektion. Hier ist keine Logik mehr untergebracht. Es geht nur darum, die Felder in die richtige Reihenfolge für die OUTTAB zu bringen.

Zusammenfassung

Diese Guidelines setzen Leitplanken, innerhalb derer man sich sicher bewegen kann. Es beschreibt viele unterschiedliche Aspekte, die bei der AMDP und SQLScript Entwicklung beachtet werden sollen. Es sollte nur aus guten Gründen davon abgewichen werden.

Sie erklären aber nicht die grundlegenden Konzepte hinter der Programmiersprache SQLScript und dem AMDP Framework, das Vorgehen bei der Erstellung von Transformationsroutinen und all die technischen Details, die man kennen sollte, um gute Transformationsroutinen zu schreiben. Dafür ist eine fundierte Ausbildung der Entwickler notwendig.

Feedback erwünscht

Ich freue mich über jedes Feedback und fruchtbare Diskussionen zu diesen Themen. Bitte schicken Sie mir eine E-Mail oder nutzen das Kontaktformular, wenn Sie Anmerkungen dazu haben.