ADMIN

2022

09

2022-08-30T12:00:00

Datenbanken und Applikationen

SCHWERPUNKT

074

Datenbank

Datenbanken passend entwerfen und einrichten

Richtigrum aufgezäumt

von Martin Loschwitz

Veröffentlicht in Ausgabe 09/2022 - SCHWERPUNKT

Datenbanken finden sich heute fast in jeder IT-Anwendung. Doch kommen MariaDB, PostgreSQL & Co. viel zu oft in einer Art und Weise zum Einsatz, die allen ursprünglichen Ideen der jeweiligen Autoren widerspricht. Das sorgt spätestens dann für Ärger, wenn das Ende der Fahnenstange in Sachen CPU und RAM erreicht ist. Worauf Admins und Entwickler beim Design und der Implementierung von Datenbanken achten sollten, damit es nicht so weit kommt, verrät unser Artikel.

Der Ausdruck "Du hältst es verkehrt" hat sich in der IT mittlerweile zum Bonmot entwickelt, falls ein Anwender das Fehlverhalten eines Programms kritisiert. Zurück geht der Spruch auf den einstigen Apple-Chef Steve Jobs, der US-amerikanischen iPhone-Nutzern auf deren Beschwerden hinsichtlich der miserablen Empfangsqualität früherer iPhone-Modelle entgegnete, sie hielten die Geräte halt einfach falsch in ihren Händen. Oft genug steckt in der Diagnose "You're holding it wrong" allerdings mehr als nur ein Fünkchen Wahrheit – denn es gibt viele Standardanwendungen, die beinahe jede Umgebung enthält und deren Nutzung vielerorts alles andere als optimal umgesetzt ist.
Datenbanken sind dafür ein hervorragendes Beispiel: MariaDB oder PostgreSQL und selbst NoSQL sind schnell aufgesetzt und integriert. Doch erledigen diese Arbeiten regelmäßig Menschen, die keine ausgewiesenen Datenbankprofis sind, und oft genug mit fatalen Konsequenzen, die erst viel später sichtbar werden: dann nämlich, wenn eine Datenbank bereits bei verhältnismäßig kleiner Last in die Knie geht. Wenn das in solchen Situationen oft reflexartig praktizierte Skalieren in die Höhe mangels stärkerer CPU oder zusätzlicher RAM-Slots ebenso wenig klappt, ist die Katastrophe perfekt. Denn oft genug ist es unmöglich, ein gegebenes Datenbankdesign einer App im laufenden Betrieb zu reparieren oder auch nur zu verändern.
Kluges Design einer Datenbank setzt de facto viel früher als erst dann an, wenn das Gerät unter der anliegenden Last bereits zusammenzubrechen droht – nämlich bei der Planung der Art und Weise, wie die Datenbank zu nutzen ist, welche Daten anfallen und wie sich das Schema der Datenbank sinnvoll implementieren lässt. Der folgende Artikel listet ohne Anspruch auf Vollständigkeit einige der gängigsten Fehler bei der Verwendung von Datenbanken auf und erklärt, wie sich die jeweiligen Fehler im Vorfeld durch kluge Planung vermeiden lassen.
Der Ausdruck "Du hältst es verkehrt" hat sich in der IT mittlerweile zum Bonmot entwickelt, falls ein Anwender das Fehlverhalten eines Programms kritisiert. Zurück geht der Spruch auf den einstigen Apple-Chef Steve Jobs, der US-amerikanischen iPhone-Nutzern auf deren Beschwerden hinsichtlich der miserablen Empfangsqualität früherer iPhone-Modelle entgegnete, sie hielten die Geräte halt einfach falsch in ihren Händen. Oft genug steckt in der Diagnose "You're holding it wrong" allerdings mehr als nur ein Fünkchen Wahrheit – denn es gibt viele Standardanwendungen, die beinahe jede Umgebung enthält und deren Nutzung vielerorts alles andere als optimal umgesetzt ist.
Datenbanken sind dafür ein hervorragendes Beispiel: MariaDB oder PostgreSQL und selbst NoSQL sind schnell aufgesetzt und integriert. Doch erledigen diese Arbeiten regelmäßig Menschen, die keine ausgewiesenen Datenbankprofis sind, und oft genug mit fatalen Konsequenzen, die erst viel später sichtbar werden: dann nämlich, wenn eine Datenbank bereits bei verhältnismäßig kleiner Last in die Knie geht. Wenn das in solchen Situationen oft reflexartig praktizierte Skalieren in die Höhe mangels stärkerer CPU oder zusätzlicher RAM-Slots ebenso wenig klappt, ist die Katastrophe perfekt. Denn oft genug ist es unmöglich, ein gegebenes Datenbankdesign einer App im laufenden Betrieb zu reparieren oder auch nur zu verändern.
Kluges Design einer Datenbank setzt de facto viel früher als erst dann an, wenn das Gerät unter der anliegenden Last bereits zusammenzubrechen droht – nämlich bei der Planung der Art und Weise, wie die Datenbank zu nutzen ist, welche Daten anfallen und wie sich das Schema der Datenbank sinnvoll implementieren lässt. Der folgende Artikel listet ohne Anspruch auf Vollständigkeit einige der gängigsten Fehler bei der Verwendung von Datenbanken auf und erklärt, wie sich die jeweiligen Fehler im Vorfeld durch kluge Planung vermeiden lassen.
Der Klassiker: Falsche Daten
Die Feststellung, dass die in einer Datenbank zu speichernden Daten idealerweise zur Grundstruktur der Datenbank passen sollten, ist eigentlich eine Binsenweisheit. Umso erstaunlicher ist es, wie viele IT-Abteilungen ihre Datenbanken täglich mit Informationen quälen, die der ursprünglichen Struktur der genutzten Datenbank eigentlich komplett zuwiderlaufen.
Performancewerte sind der Klassiker: Wer von vielen Messpunkten regelmäßig viele einzelne Messwerte ausliest und speichern möchte, nutzt für diese Aufgabe idealerweise eine sogenannte Zeitreihendatenbank. Die unterscheidet sich von einer klassischen relationalen Datenbank vor allem dadurch, dass ihr grundlegendes Element eben nicht die Einteilung in Tabellen und Reihen ist, sondern ein Zeitstrahl, zu dem sich beliebige Werte an beliebigen Punkten addieren lassen.
Das spielt insbesondere eine Rolle, wenn es später um das Auslesen von Daten geht, wie ein einfaches Beispiel verdeutlicht: Soll über einen Zeitraum von zwei Wochen aus einer relationalen Datenbank der Wert einer bestimmten Metrik ausgelesen werden, passiert das meist per SELECT-Statement in Kombination mit einer LIKE-Einschränkung. Dabei unterschätzen Admins regelmäßig, wie viele Messwerte sie beim Monitoring erzeugen: Wer in einer Umgebung alle 20 Sekunden 150 Werte pro Host ausliest und 500 Hosts hat, erzeugt in der Datenbank pro Sekunde im Schnitt fast 4000 Einträge in Tabellenzeilen, also etliche Millionen Einträge pro Tag. Soll dann ein spezifischer Teilbereich ausgelesen werden, muss die Datenbank buchstäblich jede Zeile einlesen, den Filter anwenden und bestenfalls in das Ergebnis übernehmen.
Anders bei einer Zeitreihendatenbank: Hier gibt die Datenbank einfach alle Werte wieder, die für die genannte Metrik innerhalb des definierten Zeitraums um den Zeitstrahl herum angegeben sind. Dabei sind nicht nur die Geschwindigkeitsunterschiede zwischen Zeitreihendatenbanken (Bild 1) und ihren relationalen Pendants frappierend, sondern auch die in der Software jeweils erzeugte Last. Die erste Lektion lautet folglich: Lange vor der Entscheidung für ein bestimmtes Produkt in Sachen Datenbank muss klar sein, welche Anforderungen die zu speichernden Daten an die Datenbank stellen. Erst dann kann eine Vorauswahl nach Typ erfolgen, etwa für Zeitreihendatenbanken, relationale Datenbanken oder Key-Value-Stores.
Letztere spielen in der geistigen Gegenwart vieler Administratoren ohnehin eine viel zu kleine Rolle. Denn natürlich lässt sich auch eine relationale Datenbank als simpler Key-Value-Store nutzen. Doch ist die Situation dann vergleichbar mit Anwendungen, die auf POSIX-Dateisysteme zugreifen, ohne den größten Teil der POSIX-Garantie zu benötigen: Viel Performance geht für jene Garantien verloren, ohne dass die Anwendung oder der Nutzer irgendwas davon hätten. Wer nur Pärchen aus Schlüssel und Wert speichern möchte, sollte dringend evaluieren, ob Werkzeuge wie Cassandra oder gar Etcd eine Alternative zu ausgewachsenen Datenbanken sind.
Bild 1: Wer solche Metrik- und Performancedaten speichern möchte, nutzt besser eine dafür optimierte Zeitreihendatenbank anstelle von MariaDB oder PostgreSQL
Fehlende Normalisierung
Der zweite Platz auf der Don'ts-Liste gängiger Fehler im Umgang mit Datenbanken erscheint so banal und so weit verbreitet, dass selbst erfahrene Datenbank-Jockeys immer wieder ins Staunen geraten. Unter dem Begriff "Normalisation" oder "Normalisierung" verstehen Datenbankde­signer die Arbeit, bereits im Vorfeld der Nutzung einer Datenbank das Schema festzulegen, das eine Anwendung später nutzt. Gerade bei relationalen Systemen wie MariaDB oder PostgreSQL spielt die Normalisierung eine herausragende Rolle. Denn die "Simple Query Language", SQL, die diesen Anwendungen zugrunde liegt, ist auf eine spezifische Syntax bei Datenbankabfragen hin optimiert.
Das Kernprinzip der Syntax ist, dass jeder zu speichernde Schlüssel mit idealerweise einem spezifischen Wert in einer einzelnen Zeile einer Datenbank stehen soll, sodass der Zugriff anhand festgelegter Parameter möglich wird. Um einzelne Werte aus verschiedenen Tabellen derselben Datenbank miteinander zu verbinden, kennt SQL sogar eigene Anweisungen, namentlich etwa JOIN. Es könnte also alles so einfach sein.
Dem ist in der Praxis aber oft nicht so. Statt der sauberen und stringenten Struktur zu folgen, die in SQL angelegt, ja fast erzwungen ist, weichen nicht wenige Datenbankentwickler das Prinzip auf und kombinieren etliche Werte eines einzelnen Schlüssels in einer Datenbanktabelle. Zum Einsatz kommen anstelle von JOIN-Statements dann riesige SELECT-Befehle, die regelmäßig mit dem Parameter LIKE arbeiten und die Datenbank dadurch zu Manövern zwingen, die in SQL so eigentlich gar nicht vorgesehen sind.
Je größer die Tabellen einer Datenbank geraten und je komplexer die Struktur innerhalb einer Tabelle ist, desto ressourcenintensiver werden LIKE-Statements (oder Befehle mit CHARINDEX und Co.) auf meist ohnehin schon riesige SELECT-Ergebnisse. Die Datenbank quittiert das früher oder später mit absurd hoher Systemlast und schier endlosen Verarbeitungszeiten selbst für banale Anfragen – bis hin zur kompletten Unbenutzbarkeit.
Viel sinnvoller wäre stattdessen ein Datenbankdesign, das dem Mantra "Ein Wert pro Zeile, eine Zeile pro Spalte" folgt und die SQL-Möglichkeiten wie JOIN nutzt. Dass dies nicht passiert, ist regelmäßig entweder Zeitnot oder Faulheit geschuldet: Einerseits bedingt ein klug erdachtes Datenbankkonzept Vorbereitungszeit, die in vielen Projekten fehlt. Andererseits setzt ein komplexes Datenbankdesign mit vielen Tabellen mehr Code auf Seiten der Anwendung voraus, um zuverlässig die richtigen Werte auszulesen. Wer sich die Zeit bei der Planung und der Entwicklung nimmt, bekommt regelmäßig die Dividende in Form hoher Datenbankperformance bei selbst mittelmäßiger Hardware und muss sich keine Gedanken darüber machen, wie er im laufenden Betrieb auch noch das letzte bisschen verfügbarer Performance aus MariaDB herauskitzelt.
Namenskatastrophen vermeiden
Zumindest indirekt mit dem Thema Normalisierung zusammenhängt das Namensschema, für das ein Entwickler sich beim Design seiner Datenbank entscheidet. Hier begegnen dem IT-Fachmann regelmäßig Konstruktionen, die mit jedem Recht die Bezeichnung "aberwitzig" verdienen: Tabellen etwa, die "Kunde" heißen und dann 15 Felder enthalten, von denen zehn mit so vielsagenden Begriffen wie "val1" bis "val10" versehen sind. Ihren Ursprung haben solche absurden Namenskonventionen meist bei den Entwicklern, und selten entstehen sie in böser Absicht. "checkid" könnte etwa die Abkürzung für "value" sein, und mittels der Felder könnte eine Funktion implementiert sein, die verschiedene Parameter zu einem Benutzeraccount enthält, weil die jeweilige Anwendung es eben so vorgibt. Hier ergeben sich allerdings gleich mehrere Probleme.
Einerseits verstößt das beschriebene Datenbankdesign gegen den zuvor erklärten Grundsatz der Normalisierung. Sinnvoller wäre es, eigene Tabellen für die Parameter zu haben und diese per JOIN-Statement miteinander zu verbinden oder aus ihnen heraus gleich aufeinander zu referenzieren. Fast ebenso fatal ist aber, dass der Entwickler und vor allem der Admin mit nichtssagenden Feldbezeichnungen wie "val" im Alltag kaum etwas anfangen können. Sind Daten händisch aus der Datenbank zu fischen, geht die große Sucherei nach den eigentlichen Bezeichnungen der Felder los. Das nervt nicht nur, sondern es unterstützt wie beschrieben auch ein Datenbank-Layout, das gerade für SQL-basierte Datenbanken alles andere als optimal ist.
Klug ist es stattdessen, bereits ganz am Anfang der Entwicklung ein Namensschema [1] für eine Datenbank zu definieren und dieses konsequent durchzuhalten. Freilich hilft es, möglichst aussagekräftige Namen für einzelne Tabellenspalten zu nutzen – ob dabei dann CamelCase oder ein anderes Schema zum Einsatz kommt, hängt vorrangig vom Geschmack der handelnden Personen ab.
Eine Tabelle, sie zu knechten!
Selbst wenn es unglaublich scheint: Es existieren immer noch Datenbanken, in denen sämtliche Parameter in einer einzelnen, riesigen Tabelle untergebracht sind. Über Sinn und Unsinn eines solchen Ansatzes ist nach den vorherigen Abhandlungen über Normalisierung in Datenbanken keine Diskussion mehr nötig; regelmäßig liegt der Grund für die Vorgehensweise darin, dass ein solches Datenbanklayout gerade in der Anfangszeit einer Programmentwicklung relativ wenig Aufwand auf der Entwicklerseite bedingt.
Welchen Moloch sie sich hier eigentlich erschafft haben, merken viele Admins und Entwickler erst, wenn sie vor lauter LIKE- und CHARINDEX-Statements in ihrem Tool den Wald vor lauter Bäumen nicht mehr sehen und die Antwortzeit der Datenbank zudem biblische Dimensionen erreicht. Wer mit einem derartigen Datenbank-Kollateralschaden konfrontiert ist, hat kaum noch eine Chance, das Problem unter vertretbarem Aufwand im laufenden Betrieb zu reparieren. Denn anders als bei den zuvor beschriebenen Szenarien, wo zumindest manchmal mit wenig Aufwand Verbesserungen zu erreichen sind, ist in einem solchen Fall nichts zu retten. Hier ist stattdessen ein umfangreiches Post-Mortem samt Redesign der Datenbank notwendig, um die Situation wenigstens in der Zukunft zu verbessern.
Fehlende Indexe rächen sich
Praktisch alle modernen Datenbanken und besonders jene, die irgendeinen SQL-Dialekt sprechen, unterstützen das sogenannte Indexing. Um dessen technische Grundlagen zu verstehen, ist ein kleiner Ausflug in den Unterbau gängiger SQL-Datenbanken nötig. Zunächst gilt: Noch immer existieren heute äußerst viele Anwendungen am Markt, die eher leselastig sind, die also viel mehr aus einer Datenbank lesen, als sie schreiben. Der Vorgang, bestehende Tabellen in Datenbanken um neue Datensätze zu erweitern, ist aber natürlich trotzdem von elementarer Bedeutung.
Zwei Standardmodelle haben sich etabliert, um diesen Vorgang durchzuführen: Bei der Verwendung von "Heap Tables" werden neue Datensätze an bestehende Datenbanken angehängt, ohne dabei irgendeiner speziellen Ordnung zu folgen. Der Schreibperformance ist das ausgesprochen dienlich, der Leseperformance hingegen nicht – denn wenn aus einer solchen Tabelle bestimmte Daten zu lesen sind, muss sie komplett durchsucht werden. Während bei einer solchen Tabelle das Einfügen also in drei Millisekunden erledigt sein kann, kann die Suche nach den gerade erst eingefügten Daten 150 Millisekunden und mehr in Anspruch nehmen – ein Faktor von 50 (Bild 2).
Bild 2: In einer Heap Table ohne Index dauert das Einfügen von Daten nur wenige Millisekunden, während das anschließende Auslesen fast 50 mal mehr Zeit in Anspruch nimmt.
"Heap Tables" lassen sich durch das manuelle Anlegen eines Indexes erheblich flotter machen. Hier bestimmt der Datenbankadministrator, welchen Parameter er bei der Suche in einer Tabelle nutzen möchte, und legt für diesen Parameter einen Index an. Die Datenbank muss bei einer späteren Suche dann nicht mehr den Inhalt der Tabelle durchsuchen, sondern fischt anhand des Indexes die passende Zeile aus der Tabelle. Das geht im Normalfall viel schneller. Pro Tabelle lassen sich bei den allermeisten Datenbanken auch mehrere Indexe definieren, die anschließend mittels der in SQL zur Verfügung stehenden Statements wieder miteinander zu kombinieren sind (Bild 3).
Die Alternative zu "Heap Tables" ist die "Index Organized Table" (IOT), bei der Indexe auf Basis sämtlicher Inhalte der Datenbank zu deren Grundstruktur gehören. IOTs sind gegenüber schlanken Heap-Tabellen mit händisch angelegten Indexen nicht automatisch im Vorteil, viel eher kommt es hier auf den Einzelfall an. Etwas experimentieren schadet im Falle eines Falles nicht, solange der zuständige Datenbankadministrator das Thema Indexe überhaupt auf der Uhr hat und diese schlau zum Einsatz bringt.
Bild 3: Dieselben Daten wie in Bild 2, diesmal mit Index: Dank des Indexes reduziert sich die Zeit, die die Datenbank zum Auslesen der Daten braucht, auf einen Bruchteil.
Redundante Daten helfen nicht
Wenn von Automation- und Lifecycle-Management-Werkzeugen die Rede ist, fällt in diesem Kontext oft der Begriff der "Single Source of Truth". Diesen gibt es auch im Datenbankkontext durchaus, doch meint er hier etwas anderes: den Ansatz nämlich, jede zu speichernde Information nur exakt einmal in der Datenbank zu haben. Haben Datenbankadministratoren eingangs nicht auf ihre Normalisierung geachtet oder ziehen aus dem Problem fehlender Normalisierung die falschen Rückschlüsse, speichern sie dieselben Daten oft an unterschiedlichen Stellen in derselben Datenbank. Dann gibt es etwa eine Tabelle "Kunden" mit den einzelnen Kunden und einer festgelegten ID, die aus der Shop-Software heraus aber gleich noch in etliche andere Tabellen der Datenbank geschrieben wird, um dort mit ihr ebenfalls Parameter zu assoziieren.
Was gut gemeint ist, führt zu mehreren Problemen. Einerseits bläht das Vorgehen den in der Datenbank vorhandenen Datenschatz unnötig auf, was wiederum in Performanceproblemen resultieren kann. Andererseits machen Menschen beim Entwickeln Fehler, und doppelt vorhandene Daten erhöhen die Gefahr, falsch miteinander verbunden zu werden. Praktisch könnte das etwa dazu führen, dass dem Kunden mit der ID 15 in der "order"-Tabelle andere Bestellungen zugeordnet sind, als er tatsächlich ausgelöst hat, und in der Tabelle "customerData" stehen dann womöglich auch noch falsche Adressdaten.
Wenn eine solche Katastrophe einmal ihren Lauf genommen hat, ist sie nachträglich kaum noch zu korrigieren. Einmal mehr gilt an dieser Stelle deshalb der eherne Grundsatz: Lieber die in SQL ohnehin vorhandenen Wege nutzen, um Felder in verschiedenen Tabellen miteinander zu verknoten, statt auf eigener Faust eine Lösung für ein Problem zu erdenken, das in SQL bereits gelöst ist.
Mangelhafte Dokumentation
Wer kennt es nicht: erster Arbeitstag im neuen Job, die Euphorie ist groß, die neuen Kolleginnen und Kollegen haben gleich eine spannende Aufgabe und das Leben ist schön. Flugs haut der Admin also eine Zeichenfolge in die Tastatur, die das jeweilige Problem nach Monaten endlich löst. Fünf Jahre später steht die IT-Abteilung vor der nächsten Herausforderung, die Euphorie ist dem Alltag gewichen und der Nachfolger auf der eigenen Position will adäquat eingearbeitet werden. Beim Öffnen des vor fünf Jahren veränderten Programms weiß jedoch keiner mehr, was genau dort eigentlich passiert. Kommt nicht vor? Oh doch: In einer Umfrage des amerikanischen Anbieters Altexsoft gaben im April 2022 die mit Datenbanken betrauten Admins an, dass der zeithäufigste Grund für fundamentale Probleme bei ihrer Arbeit fehlende Dokumentation ist.
Wer schon ein paar Jahre Systemadministration auf dem Buckel hat, kennt das Problem meist, nicht nur aus dem Datenbankbereich. Hier jedoch kann sich fehlende Dokumentation als ein besonders tückisches Problem herausstellen. Wenn unklar ist, was der aktuelle Code tut und wieso die Datenbank im Augenblick so aussieht, wie sie aussieht, sind Änderungen ohne erhebliche Gefahr für den Live-Betrieb kaum durchzuführen.
Datenbanken gehören aus eben diesen Gründen genauso gut dokumentiert wie alle anderen Teile einer IT-Umgebung. In die Datei, mit der das ursprüngliche Schema einer Datenbank erstellt worden ist, gehört für jede Tabelle und für jede derer Spalten eine Erklärung über Sinn und Zweck der Eigenschaften. Finden später Änderungen des Schemas statt, sind diese ebenfalls zu dokumentieren. Es schadet nicht, eine schematische Beschreibung der aktuellen Datenbank samt aktueller Kommentare in Git vorzuhalten, weil das neuen Kollegen die Einarbeitung erleichtert und auch ein lokales Test-Setup mit Beispieldaten erlaubt.
Fazit
Die meisten Probleme im Kontext von Datenbanken gehen auf banale Ursachen zurück: Fehlende Dokumentation, unvollständige oder sinnlose Namenskonventionen sowie eine Datenbank in Form einer einzelnen Tabelle sind heute weder notwendig noch entschuldbar. Wer sich am Anfang des Lebenszyklus einer Datenbank nicht die Zeit nimmt, solche Fehler zu vermeiden, investiert später enorme Mühen in Stresssituationen, um die aus den eingangs gemachten Fehlern resultierenden Probleme zu reparieren. Gut geplant ist bei Datenbanken halb gewonnen.
Das gilt insbesondere für das Thema der Normalisierung. Zweifelsohne ist die Normalisierung einer Datenbank aber auch der Teil beim Design, der am schwierigsten "richtig" zu erledigen ist. Wenn alle Stricke reißen, kann es nicht schaden, zu Beginn der Entwicklung einer Anwendung Datenbankdesigner zu Rate zu ziehen. Das mag anfänglich zwar ein Loch ins Budget reißen, schont dieses später aber in Form von Hardware.
(ln)
Link-Codes
[1] Standards für Datenbank-Namensschemas: https://dev.to/ovid/database-naming-standards-2061/