- SQL-Antipatternen erschweren die Wartung von Queries und Datenpipelines und führen zu langsamerer Performance als erwartet
- Typische Beispiele sind übermäßiger Einsatz von CASE WHEN, Funktionen auf indexierten Spalten, SELECT *, übermäßiger Einsatz von DISTINCT, verschachtelte Views und Subqueries sowie tiefe Abhängigkeitsstrukturen
- Die meisten Probleme entstehen durch provisorische Lösungen unter Zeit- und Termindruck und schaden langfristig sowohl der Datenzuverlässigkeit als auch der Entwicklungsgeschwindigkeit
- Als Gegenmaßnahmen braucht es unter anderem klar definierte Joins, den Einsatz von Dimensionstabellen, das Entfernen unnötiger Verschachtelungen und eine regelmäßige Bereinigung von Views
- SQL sollte nicht als einfaches Skript, sondern als im Team verwalteter Produktionscode behandelt werden; eine auf Lesbarkeit bedachte Anfangsplanung reduziert Nacharbeit
Einführung
- Heute geht es um einige häufige und besonders folgenreiche SQL-Antipatternen
- Diese Probleme führen zu einem Teufelskreis aus sinkender Datenzuverlässigkeit und langsamerer Query-Entwicklung
- Die folgende Liste ist nicht vollständig; für ein tieferes Verständnis empfiehlt sich Bill Karwins Buch
Übermäßig komplexe CASE-WHEN-Ausdrücke
- In großen Systemen werden CASE-WHEN-Ausdrücke häufig verwendet, um Statuscodes (z. B. 1 = nicht auf Lager) in menschenlesbare Texte umzuwandeln
- Für die schnelle Entwicklung eines Dashboards oder Reports diese Logik nur in einer einzelnen View unterzubringen, ist langfristig ein Antipattern
- Das führt zu duplizierter Copy-and-paste-Logik, widersprüchlichen Interpretationen und macht die gesamte Query-Landschaft unübersichtlich
- Die Lösung ist, eine separate Dimensionstabelle (dimension table) oder eine gemeinsame View zur Umwandlung der Statuscodes zu erstellen, damit die Logik wiederverwendbar wird
Funktionen auf indexierten Spalten verwenden
- Wenn man wie in
WHERE UPPER(name) = 'ABC' eine Funktion auf eine indexierte Spalte anwendet, geht die Effizienz des Indexes verloren
- In SQL Server und ähnlichen Systemen kann das zu einem unnötigen Full Table Scan führen
- Die Lösung besteht darin, die funktional transformierte Spalte separat zu indexieren oder den Eingabewert vorab umzuwandeln, um die Query-Bedingung zu vereinfachen
SELECT * in Views verwenden
- Bei der Entwicklung von Views wirkt SELECT * bequem, aber wenn sich die Struktur (das Schema) ändert, kann die View leicht kaputtgehen
- Außerdem werden unnötige Spalten mitgezogen, was zu unbeabsichtigten Abhängigkeiten und Performance-Problemen führt; deshalb sollten Spalten explizit ausgewählt werden
Mit DISTINCT Duplikate „lösen“
- Wenn durch fehlerhafte Joins doppelte Ergebnisse entstehen, ist eine provisorische Behebung mit SELECT DISTINCT problematisch, weil sie Probleme mit der Datenintegrität verdeckt
- Die eigentliche Ursache sind unvollständige Join-Bedingungen oder Fehler in der Beziehungsdefinition (1:1, 1:N usw.)
- Die richtige Lösung ist, die Beziehungsdefinition durch robustere Join-Logik zu präzisieren und die Konsistenz der Beziehungen vor Aggregation oder Reporting sicherzustellen
Verschachtelte Views (Excessive View Layer Stacking)
- Wenn mehrere Teams bestehende Views wiederverwenden und immer neue Views darauf aufbauen, wird die Abhängigkeitskette komplex und die Performance verschlechtert sich drastisch
- Debugging wird schwieriger, und das Erweitern von Queries fühlt sich bald wie eine „archäologische Ausgrabung“ an
- Nötig ist eine Strategie, die Transformationslogik regelmäßig zu flatten, und komplexe Berechnungen in klaren Basis-Views oder Tabellen zu materializen
Zu tief verschachtelte Subqueries
- Tief verschachtelte Subqueries mit mehr als 3 bis 4 Ebenen verschlechtern die Lesbarkeit und erschweren das Debugging
- Es gibt sogar Fälle mit Subqueries von mehr als 5000 Zeilen
- Mit CTEs (Common Table Expressions) lassen sich logische Schritte leichter trennen und die Lesbarkeit (readability) der Query verbessern
Fazit
- SQL wirkt oberflächlich betrachtet einfach, aber mit wachsender Systemgröße nimmt auch die Komplexität zu
- Die meisten Antipatternen entstehen nicht aus böser Absicht, sondern als Ergebnis von Kompromissen für „schnelle Ergebnisse“ (Zeitdruck, Deadlines, Provisorien)
- Wenn man SQL wie Code verwaltet (Versionsverwaltung, Code-Reviews, klare Architektur), lassen sich langfristig Produktivität und Zuverlässigkeit zugleich sichern
- Wer schon in der frühen Planung ein paar Minuten in Klarheit und Konsistenz investiert, reduziert spätere Nacharbeit und Verwirrung erheblich
5 Kommentare
Weil gerade keine Zeit ist, löscht man erstmal nur den dringendsten Brand und nimmt sich vor, es später neu zu schreiben — aber wenn sich das aufstaut, endet es in einer schrecklichen Query-Hölle. Ich habe selbst einige davon gebaut. Obwohl man weiß, dass dieses „später“, in dem man sie neu schreibt, niemals kommen wird.
Uff ...
„Die meisten Probleme sind provisorische Notlösungen aufgrund von Zeit- und Termindruck“
Schnief..
Hacker-News-Kommentar
DISTINCTsehe, vermute ich, dass der Verfasser das Datenmodell oder die Mengenlehre nicht richtig verstanden hat – oder vielleicht beides nichtDISTINCTauch ein Signal für ein übermäßig normalisiertes Schema. Zum Beispiel halte ich es nicht für nötig, extra eine Tabelle wieaddresses_citiesanzulegen, nur um zu verhindern, dass derselbe Stadtname mehrfach gespeichert wirdDISTINCTin eine CTE eingefügt habe, obwohl alle Joins korrekt waren. Wenn die Eindeutigkeit der Datensätze garantiert ist, scheint der Query Planner zu optimierenLIMIT 1zu einer Query hinzugefügt, weil ich erwartet hatte, dass höchstens ein Ergebnis zurückkommt, und bekam ähnliches Feedback, dass das keine gute Praxis sei. In großen Tabellen neigt die DB aber dazu, selbst dann weiter die ganze Tabelle zu durchsuchen, wenn der gewünschte Datensatz schon gefunden wurde (sqlite,mysql,postgresql)DISTINCTin einer Query wieSELECT x FROM tgefahrlos weglassen kann. Selbst wenn man im Schema vontsieht, dassxeinePRIMARY- oderUNIQUE-Constraint hat, kann jemand dieUNIQUE-Constraint kurz darauf entfernen. Dann entstehen Duplikate, und man rätselt, warum. SQL ist keine Set-, sondern eine Bag-Sprache. Wenn zur Laufzeitrel tund Attributxgefunden werden, werden sie einfach zurückgegeben. Es können Duplikate entstehen, und sogar Typen können sich ändern. Wenn man also ein Set will, muss manDISTINCTausdrücklich angeben. Der Query Planner wird zur Laufzeit beiUNIQUEoderPRIMARYohnehin keine Deduplizierung ausführenneo4jkomplexe Daten verarbeitet, landen doppelte Knoten sehr leicht im Ergebnis, sodassDISTINCTunverzichtbar ist. Vor allem bei Beziehungen variabler Länge wird es ohneDISTINCTlangsam und duplikatreichDISTINCTentwirfthttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
!=oderNOT IN (...)in den meisten Fällen ineffizient (es sei denn, andere Bedingungen haben die Ergebnismenge bereits stark eingegrenzt). Und es ist wichtig zu verstehen, wie die DB mitnullumgeht. Obnullund leere Zeichenkette gleich sind, obnull == nullgilt – das kann je nach DB unterschiedlich seinnull-Verarbeitung und Indizierung angeht: Die von mir verwendeten DBs indexierennull-Werte nicht, daher laufen Queries wieWHERE col IS NULLineffizient, selbst wenn es einen Index aufcolgibt. Wenn es wirklich nötig ist, empfehle ich, einechar(1)- oderbit-Spalte anzulegen, die markiert, obcolnullist, und genau dieses Feld zu indexieren!=oderNOT IN (...)fast immer ineffizient sind – ich frage mich, warum. Wenn die rechte Seite konstant ist, sollte das doch ein Hash-Table-Lookup sein und im Allgemeinen effizient laufen. Gibt es eine effizientere Alternative?DISTINCTnötig ist, könnte das ein Zeichen dafür sein, dass das Primärschlüssel-Design nicht stimmt. Wenn man zu viele Views übereinander stapelt, ist am Ende das Design der Basistabellen das Problem. Gutes DB-Modellierung verhindert all diese Probleme im Vorfeldhttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
sargablewar diese StackOverflow-Antwort hilfreich: https://dba.stackexchange.com/a/217983Das Wort
sargableist ein Portmanteau aus „Search ARGument ABLE“CASE WHENlassen sich lösen, indem man die Logik in einer UDF (User Defined Function) zentralisiertEine Funktion auf einer indexierten Spalte ist ein Signal dafür, dass die Query nicht sargable ist
Statt
DISTINCTzu überstrapazieren, kann bei durch Joins verursachtem Fan-out eine Query nützlich sein, die entsprechend der Tabellen-Granularität dedupliziert, etwa mit Manche DBs unterstützenQUALIFY, wodurch die Query deutlich sauberer wirdsargable erklärt
QUALIFY in Redshift
sqlitesehe ich das sowhere-Klauseln schreiben und Dinge wievoid/Rückgaben/Stornierungen jedes Mal erneut behandeln; bei Änderungen müsste man dann Dutzende Views/Procedures anpassen. In unserem Fall sind verschachtelte Views deutlich praktischerUPPER(name)anzulegen) ist zumindest in MS SQL Server nicht optimal. Ich weiß nicht, wie gut andere DBs das unterstützen, aber eine bessere Lösung ist, stattdessen direkt eine berechnete Spalte mit case-insensitiverCOLLATEanzulegen (je nach Geschmack anpassen)Das Wichtigste fehlt noch.