© Excellent backgrounds/Shutterstock.com
Mehr als eine Abfragesprache

SQL im 21. Jahrhundert


Die Einführung neuer QL-Sprachen erfreut sich momentan großer Beliebtheit. Erst im Sommer machte Amazon mit der Einführung von PartiQL einen neuen Vorstoß in diese Richtung. Bei diesem QL-Wildwuchs darf man das Original aber nicht aus den Augen verlieren. Was wurde eigentlich aus SQL? Steckt es tatsächlich noch in den 90ern fest? Natürlich nicht. Ein Update für Entwickler.

Das letzte Jahrzehnt war von neuen Datenbankkonzepten rund um das Thema NoSQL geprägt. Anfangs diente SQL dabei als Antithese, später wurde NoSQL als Backronym für „Not only SQL“ definiert. Zuletzt gab es dann eine regelrechte Explosion neuer QL-Sprachen. Viele davon erinnern nicht nur dem Namen nach, sondern auch der Syntax nach an SQL.

Das Versprechen dieser QL-Sprachen ist einfach: Die bekannte Grundstruktur erleichtert den Start, die punktuellen Erweiterungen liefern nach, was bei SQL fehlt. Dieses Versprechen hält einer genauen Prüfung aber nicht immer Stand. Denn zwischen dem SQL-92-Funktionsumfang, den viele aus Studium, Kursen oder Büchern kennen, und modernem SQL liegen Welten. So wurde das relationale Korsett von SQL zum Beispiel schon vor 20 Jahren aufgegeben.

Um diese und andere Überraschungen geht es in diesem Artikel. Um den Unterschied zwischen SQL und anderen Datenzugriffsmethoden zu verdeutlichen, teile ich den Funktionsumfang von SQL in vier Niveaustufen ein. Erst auf der letzten Stufe wird es um modernes SQL gehen. Die ersten drei Stufen handeln von älteren SQL-Funktionen, deren Nutzen im NoSQL-Jahrzehnt vielfach in Vergessenheit geraten ist – daher eine kurze Einführung.

Stufe 0: CRUD

Am Anfang jedes Datenzugriffs stehen die vier Grundoperationen Create, Read, Update und Delete – zusammen kurz CRUD. SQL stellt diese Grundoperationen über die Anweisungen Insert, Select, Update und Delete zur Verfügung. Das heißt aber nicht, dass diese Anweisungen jeweils nur eine dieser Grundoperationen umsetzen.

Der Unterschied zwischen reinem CRUD-Denken und SQL wird beim Inkrementieren eines Zählers deutlich. Im CRUD-Denken liest man dafür zuerst den aktuellen Zählerstand ein (Select), erhöht ihn dann in der Applikation, um den neuen Wert schließlich zu speichern (Update).

Denselben Effekt kann man in SQL aber auch mit einer einzelnen Update-Anweisung erreichen:

UPDATE Tabelle SET Spalte = Spalte + 1 WHERE ID = ?

Der Lesezugriff steckt zusammen mit der Inkrementierung im rechten Argument der Zuweisung. Dort kann man beliebig komplexe Ausdrücke verwenden – von einfachen Formeln über bedingte Ausdrücke (Case) bis hin zu Unterabfragen.

Auch Delete kann mehr als nur bestimmte Zeilen löschen. Da die Where-Klausel nicht auf Schlüsselwerte beschränkt ist, kann eine Delete-Anweisung selbst herausfinden, welche Zeilen zu löschen sind. Im folgenden Beispiel sind das jene Zeilen der Quelltabelle, deren ID in der Zieltabelle vorhanden ist:

DELETE FROM Quelltabelle WHERE EXISTS (SELECT * FROM Zieltabelle WHERE Zieltabelle.ID = Quelltabelle.ID )

Nicht einmal die Insert-Anweisung ist auf das bloße Anlegen neuer Zeilen beschränkt. Durch Kombination mit Select können Daten zum Beispiel kopiert und transformiert werden:

INSERT INTO Zieltabelle SELECT * FROM Quelltabelle

Verwendet man die Anweisungen Insert, Select, Update und Delete nur, um den CRUD-Zyklus über einen Schlüssel abzubilden, verwendet man zwar die Syntax von SQL, nicht aber die SQL-Philosophie. Daher habe ich diesem Muster das Niveau 0 zugewiesen – es sieht zwar aus wie SQL, ist aber eigentlich nicht SQL.

Oft entsteht das CRUD-Denken durch die Reduktion einer SQL-Datenbank auf die bloße Persistenz. Der Name JPA leistet hier einen traurigen Beitrag. Die von ORM-Tools generierten SQL-Anweisungen fallen dann auch großteils in diese Kategorie.

Stufe 1: Transaktionen

Die korrekte Verwendung von Transaktionen – selbst wenn ansonsten nur CRUD-Operationen verwendet werden – begründet bereits das nächste Niveau. Da Transaktionen gut in JPA integriert sind, kann man dieses Niveau noch leicht erreichen.

Der Grund, warum dieses Niveau dennoch nicht immer erreicht wird, liegt darin, dass der volle Funktionsumfang von Transaktionen nicht allgemein bekannt ist. Oft werden Transaktionen nur für die Alles-oder-Nichts-Logik beim Schreiben genutzt. Transaktionen ermöglichen aber auch konsistente Lesezugriffe und können vor Problemen durch Nebenläufigkeit schützen.

Zur Illustration kann wieder das Beispiel mit dem Zähler herangezogen werden. Bei der CRUD-Umsetzung werden zwei SQL-Anweisungen verwendet (Listing 1). Dabei stellt sich die Frage, was schiefgehen kann, wenn dieser Code mehrfach parallel abläuft.

Listing 1

SELECT Zaehler FROM Tabelle WHERE ID = ?; -- Erhöhung des Werts -- in der Applikation UPDATE Tabelle SET Zaehler = ? WHERE ID = ?;

Ohne weitere Vorsichtsmaßnahmen kann es zu dem als Lost-Update bekannten Problem kommen. Wenn zwei Threads den Lesezugriff (Select) gleichzeitig durchführen, werden beide denselben Zählerwert erhalten. In weiterer Folge werden beide denselben (erhöhten) Wert in die Datenbank schreiben. Letztendlich wurde der Zähler in der Datenbank also nur einmal erhöht – ein Update ging scheinbar verloren.

Zur Lösung dieses und ähnlicher Probleme bietet SQL die sogenannte Transaktionsisolation an. Damit kann der Code so ablaufen, als gäbe es keine anderen Aktivitäten in der Datenbank. Voraussetzung ist lediglich das richtige Setzen der Transaktionsgrenzen und die Verwendung des richtigen Transaktionsisolationslevels.

Die Ermittlung des richtigen Transaktionsisolationslevels ist eine Wissenschaft für sich – unter anderem, weil die angebotenen Level sowie die damit verbundenen Vor- und Nachteile produktspezifisch sind. Grundsätzlich kann man aber immer mit dem stärksten Level – Serializeable – anfangen und dann punktuell dort nachbessern, wo es nötig ist. Daher sieht der SQL-Standard Serializeable auch als Voreinstellung vor. Tatsächlich verwenden aber alle gängigen SQL-Datenbanken eine schwächere Voreinstellung – vermutlich, um in der Standardkonfiguration bessere Performance zu erreichen.

Bei einer Transaktion im Level Serializeable stellt die Datenbank sicher, dass sie nur dann erfolgreich abgeschlossen werden kann (Commit), wenn Änderungen, die andere Transaktionen währenddessen durchgeführt haben, zu keinem Ergebnis führen, das bei isolierter Ausführung unmöglich gewesen wäre. Im Beispiel von Listing 1 heißt das, dass das Commit nur dann erfolgreich sein darf, wenn die gelesenen Daten (Select) bis zum Schreiben (Update) nicht geändert wurden. Dafür muss die Datenbank natürlich wissen, dass der Lesezugriff zur Transaktion gehört – die Transaktion muss also vor dem Select beginnen und darf erst nach dem Update enden.

Das von JPA angebotene pess...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang