Wenn gute Anwendungsentwickler Datenbanken designen – Typische Fallstricke & Lösungen

„Die GUI ist perfekt, die Fachlichkeit stimmt – aber die Datenbank schreit um Hilfe.“
Typische Fallstricke und wie man sie vermeidet.

🎭 Das Phänomen: Gute Entwickler, schlechte Datenbank

ÜBERBLICK
Immer wieder treffen wir auf hochtalentierte Anwendungsentwickler: Sie beherrschen komplexe GUI-Frameworks, verstehen die Fachlichkeit bis ins kleinste Detail und liefern pünktlich. Doch ihre Datenbank-Designs offenbaren oft tiefe Verständnislücken – mit fatalen Folgen für Performance, Skalierbarkeit und Wartbarkeit. Die Ursache liegt selten in Böswilligkeit, sondern in einer anderen Denkweise: OO vs. relational, zeilenweise Verarbeitung statt Mengenlogik, ORM als „Zauberkasten“.
💡 Die gute Nachricht: Mit ein paar grundlegenden Prinzipien und etwas Sensibilisierung lassen sich die meisten Probleme vermeiden. Dieser Artikel zeigt die häufigsten Fehler, ihre (drastischen) Auswirkungen und praxistaugliche Lösungen.

1. Keine Ahnung von Normalisierung

SCHWERWIEGEND
Entwickler modellieren Tabellen oft wie Entitäten in einer objektorientierten Klassenstruktur: alles in eine Tabelle, redundante Daten, keine eindeutigen Schlüssel. Die Folge: Anomalien bei Updates, riesige Datensätze, langsame Abfragen.
-- ❌ ANTI-PATTERN: Unnormalisierte "All-in-one"-Tabelle 
CREATE TABLE Bestellungen_Unnormalisiert ( 
BestellID INT, 
KundeName VARCHAR(100), 
KundeStadt VARCHAR(50), 
ProduktName VARCHAR(200), 
ProduktPreis DECIMAL(10,2), 
Menge INT 
); 
-- Redundanz: Kundendaten wiederholen sich bei jeder Bestellposition 
 
-- ✅ BESSER: Normalisiert (Kunden, Produkte, Bestellungen, Bestellpositionen) 
CREATE TABLE Kunden (KundenID INT PRIMARY KEY, Name VARCHAR(100), Stadt VARCHAR(50)); 
CREATE TABLE Produkte (ProduktID INT PRIMARY KEY, Name VARCHAR(200), Preis DECIMAL(10,2)); 
CREATE TABLE Bestellungen (BestellID INT PRIMARY KEY, KundenID INT REFERENCES Kunden(KundenID)); 
CREATE TABLE BestellPositionen (BestellID INT, ProduktID INT, Menge INT, PRIMARY KEY (BestellID, ProduktID));
📌 Auswirkung: Die unnormalisierte Tabelle wächst exponentiell, Updates werden zum Albtraum, und Ad‑hoc‑Abfragen werden immer langsamer. Ein normalisiertes Design spart Speicher, verbessert die Konsistenz und ermöglicht saubere Joins.

2. „NVARCHAR(MAX) für alles“ – der Komfort-Typ

PERFORMANCEKILLER
Viele Entwickler speichern Zahlen, Daten und sogar kategoriale Werte in NVARCHAR(MAX). Das ist bequem, aber eine Katastrophe für Indizierung, Speicher und Vergleichsoperationen.
-- ❌ Alles als Text 
CREATE TABLE Auftrag ( 
Auftragsnummer NVARCHAR(MAX), 
Erstellungsdatum NVARCHAR(50), 
Betrag NVARCHAR(20) 
); 
-- WHERE Betrag > 100 wird zum Desaster (implizite Konvertierung + Table Scan) 
 
-- ✅ Richtige Typen 
CREATE TABLE Auftrag ( 
Auftragsnummer INT IDENTITY, 
Erstellungsdatum DATE, 
Betrag DECIMAL(12,2) 
);
⚠️ Fataler Nebeneffekt: Implizite Konvertierungen hebeln Indizes aus – der Optimizer kann trotz passendem Index keine Seek-Operation durchführen. Das kostet oft den Faktor 100 in der Ausführungszeit.

3. ORM als Allheilmittel – das N+1-Problem

LAUZEIT-FALLE
Entity Framework, Hibernate und Co. sind großartig, aber sie verleiten zu naiven Schleifenkonstrukten. Das klassische „N+1-Problem“: Erst werden alle Hauptobjekte geladen, dann für jedes Kind einzeln eine Abfrage – statt einem einzigen JOIN.
-- C#-Pseudocode (schlecht) 
var kunden = db.Kunden.ToList(); 
foreach(var k in kunden) { 
var bestellungen = db.Bestellungen.Where(b => b.KundenID == k.ID).ToList(); // 1 Query pro Kunde → N+1 
} 
 
-- Besser: Mit Include / ThenInclude (Entity Framework) 
var kundenMitBestellungen = db.Kunden.Include(k => k.Bestellungen).ToList(); 
 
-- Noch besser: Klassischer JOIN in einer View oder Stored Procedure 
SELECT k.Name, b.Bestelldatum FROM Kunden k LEFT JOIN Bestellungen b ON k.KundenID = b.KundenID;
💣 Auswirkung: Bei 1000 Kunden werden 1001 Abfragen ausgeführt – Latenz steigt linear. Datenbank und Netzwerk brechen unter der Last zusammen. Mit einem einzigen JOIN reduziert man auf 1 Abfrage.

4. Keine Indizes – oder viel zu viele („Index ALL the columns“)

SCHREIBLAST
Extreme: Entweder gibt es gar keine Indizes (jede WHERE-Klausel verursacht Table Scans) oder der Entwickler legt auf jede Spalte einen Einzelindex, ohne die Reihenfolge zu beachten.
-- ❌ Kein Index auf WHERE/JOIN-Spalten 
SELECT * FROM Bestellungen WHERE KundenID = 4711; -- Table Scan 
 
-- ✅ Richtiger Index (für Selektivität) 
CREATE INDEX IX_Bestellungen_KundenID ON Bestellungen(KundenID) INCLUDE (Bestelldatum, Summe); 
 
-- ❌ Überindizierung (zu viele Einzelindizes auf einer Tabelle) 
CREATE INDEX IX_Col1 ON Tabelle(Col1); 
CREATE INDEX IX_Col2 ON Tabelle(Col2); 
CREATE INDEX IX_Col3 ON Tabelle(Col3); -- Jeder INSERT/UPDATE muss alle Indizes pflegen
📊 Faustregel: Indizieren Sie die Filter- und Join-Spalten Ihrer wichtigsten Leseabfragen. Aber jeder zusätzliche Index kostet Schreibperformance. Bei einer Tabelle mit häufigen Inserts/Updates sind maximal 5–6 gut gewählte Indizes sinnvoll.

5. Cursor, Schleifen und RBAR (Row By Agonizing Row)

EXTREM LANGSAM
Entwickler, die aus prozeduralen Sprachen kommen, neigen dazu, Daten zeilenweise zu verarbeiten – mit Cursor oder WHILE-Schleifen. SQL ist jedoch eine Mengensprache: Ein einziger UPDATE mit CASE ist oft 100‑mal schneller.
-- ❌ Schleife im T-SQL (RBAR) 
DECLARE @id INT, @menge INT; 
DECLARE cur CURSOR FOR SELECT ProduktID, Menge FROM BestellPositionen; 
OPEN cur; 
FETCH NEXT FROM cur INTO @id, @menge; 
WHILE @@FETCH_STATUS = 0 
BEGIN 
UPDATE Produkte SET Lager = Lager - @menge WHERE ProduktID = @id; 
FETCH NEXT FROM cur INTO @id, @menge; 
END; 
 
-- ✅ Set-basierte Lösung (Mengenoperation) 
UPDATE p SET Lager = Lager - bp.Menge 
FROM Produkte p 
INNER JOIN BestellPositionen bp ON p.ProduktID = bp.ProduktID;
⏱️ Performanceunterschied: Bei 100.000 Zeilen ist die Cursor-Variante oft 100x langsamer, belegt mehr Sperren und blockiert andere Zugriffe. Die Mengenoperation läuft in Bruchteilen einer Sekunde.

6. „Constraints bremsen das Programm“ – also keine Fremdschlüssel

DATENINTEGRITÄT
Viele Entwickler deaktivieren Fremdschlüssel oder verzichten ganz auf Constraints, weil sie angeblich die Performance beeinträchtigen. Die Folge: Waisen-Datensätze, inkonsistente Daten, fehlerhafte Reports.
-- ❌ Fehlende Constraints 
CREATE TABLE Bestellungen (BestellID INT, KundenID INT); -- KundenID kann auf nicht existierenden Kunden zeigen 
 
-- ✅ Fremdschlüssel-Constraint 
ALTER TABLE Bestellungen ADD CONSTRAINT FK_Bestellungen_Kunden  
FOREIGN KEY (KundenID) REFERENCES Kunden(KundenID);
🛡️ Besser: Fremdschlüssel verhindern inkonsistente Zustände. Die minimale Performancekosten werden durch Wegfall manueller Prüfungen mehr als kompensiert. Bei sehr großen Bulk-Inserts kann man Constraints temporär deaktivieren, aber niemals weglassen.

7. Transaktionen: entweder zu lang oder gar nicht

BLOCKING & DEADLOCKS
Entwickler neigen zu übertrieben langen Transaktionen („Commit am Ende der gesamten GUI-Operation“) oder lassen Transaktionen ganz weg – beides führt zu Blocking, Deadlocks oder Datenverlust.
-- ❌ Riesige Transaktion mit Benutzerinteraktion 
BEGIN TRANSACTION; 
-- ... Datenbankänderung 1 
-- Jetzt wartet das Programm auf Benutzereingabe (Sperren bleiben aktiv!) 
-- ... Datenbankänderung 2 
COMMIT; 
 
-- ✅ Kurze, in sich geschlossene Transaktionen 
BEGIN TRANSACTION; 
UPDATE ... ; 
INSERT INTO ... ; 
COMMIT;
🔒 Daumenregel: Transaktionen sollten so kurz wie möglich sein (nur die unbedingt notwendigen DML-Operationen). Sperren Sie keine Ressourcen über Netzwerkwarten oder Benutzerdialoge.

8. Ad‑hoc SQL statt parametrisierter Queries oder Prozeduren

PLAN CACHE & SICHERHEIT
Viele ORMs generieren Ad‑hoc SQL mit Literalwerten. Das führt zu überfülltem Plan Cache, verpasster Planreuse und im schlimmsten Fall zu SQL-Injection (wenn nicht sauber parametrisiert wird).
-- ❌ Unsicheres dynamisches SQL (Anfällig für Injection) 
string sql = "SELECT * FROM Kunden WHERE Name = '" + name + "'"; 
 
-- ✅ Parametrisierte Query / Stored Procedure 
CREATE PROCEDURE dbo.GetKundenByName @Name NVARCHAR(100) 
AS 
SELECT * FROM Kunden WHERE Name = @Name;
🧠 Vorteil: Stored Procedures kapseln die Logik, fördern Wiederverwendung, verbessern die Sicherheit und ermöglichen eine bessere Berechtigungssteuerung. Außerdem bleibt der Ausführungsplan erhalten.

📋 Wie man es besser macht – eine kleine DB‑Checkliste für Entwickler

BEST PRACTICES
Nicht jeder Entwickler muss ein DBA werden, aber die folgenden Tipps vermeiden die häufigsten Gräben:
  • Normalisieren Sie mindestens bis zur 3. Normalform – danach können Sie aus Performancegründen gezielt denormalisieren.
  • Wählen Sie passende Datentypen: DATE für Daten, INT für IDs, DECIMAL für Geld, VARCHAR für Text mit begrenzter Länge.
  • Indizieren Sie fremde Schlüssel und häufig verwendete WHERE-Spalten – aber nicht wahllos.
  • Vermeiden Sie RBAR – denken Sie in Mengen (JOIN, UPDATE mit FROM, CASE).
  • Nutzen Sie Constraints (PK, FK, UNIQUE, CHECK) – sie sind Ihre Versicherung.
  • Halten Sie Transaktionen kurz und vermeiden Sie Benutzerinteraktion innerhalb einer Transaktion.
  • Bevorzugen Sie Stored Procedures oder zumindest parametrisierte Queries gegenüber Ad‑hoc SQL.
  • Nutzen Sie den Ausführungsplan – er zeigt, wo ein Index fehlt oder ein Scan stattfindet.
  • Lasttests mit realistischen Datenmengen – was in der Entwicklung mit 100 Zeilen fliegt, kann mit 1 Mio. Zeilen kollabieren.
💡 Abschließend: Die Zusammenarbeit zwischen Anwendungsentwicklern und DBAs ist kein Gegensatz. Wer bereit ist, ein paar fundamentale relationale Prinzipien zu verstehen, wird überrascht sein, wie stabil und schnell die Anwendung auch unter Last läuft.