Typische Fallstricke und wie man sie vermeidet.
🎭 Das Phänomen: Gute Entwickler, schlechte Datenbank
1. Keine Ahnung von Normalisierung
-- ❌ 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));
2. „NVARCHAR(MAX) für alles“ – der Komfort-Typ
-- ❌ 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)
);
3. ORM als Allheilmittel – das N+1-Problem
-- 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;
4. Keine Indizes – oder viel zu viele („Index ALL the columns“)
-- ❌ 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
5. Cursor, Schleifen und RBAR (Row By Agonizing Row)
-- ❌ 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;
6. „Constraints bremsen das Programm“ – also keine Fremdschlüssel
-- ❌ 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);
7. Transaktionen: entweder zu lang oder gar nicht
-- ❌ 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;
8. Ad‑hoc SQL statt parametrisierter Queries oder Prozeduren
-- ❌ 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;
📋 Wie man es besser macht – eine kleine DB‑Checkliste für Entwickler
- 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.