MariaDB - Strukturierte Datenbanken im KI-System
Strukturierte Datenbanken sind das Rückgrat jeder KI-Integration. MariaDB speichert User-Daten, Transaktionen, Logs und Konfigurationen zuverlässig und macht sie verfügbar für Ollama (LLMs), ChromaDB (Vektoren) und das gesamte System. Ohne solide strukturierte Datenhaltung bleibt KI-Integration fragil und schwer wartbar.
Inhaltsverzeichnis
GRUNDLAGEN
- Was ist MariaDB
- SQL-Datenbanken vs. Vektordatenbanken
- Kernkonzepte: Tabellen, Relationen, Transaktionen
- Warum MariaDB im KI-Stack
INTEGRATION & ZUSAMMENSPIEL
- MariaDB + Ollama + ChromaDB: Das Zusammenspiel
- Prepared Statements: Sicherer Datenbankzugriff
- MariaDB mit PHP und Python
PRAKTISCHE ARBEIT
- Installation und Setup
- Datenbanken und Tabellen erstellen
- CRUD-Operationen: Create, Read, Update, Delete
- Query-Strategien: SELECT, WHERE, JOIN, Indizes
STRUKTURIERUNG & RELATIONEN
- Normalisierung: Daten sinnvoll strukturieren
- Foreign Keys und Relationen: Datenintegrität gewährleisten
VERGLEICH & EINSATZGEBIETE
PRODUKTIV & ADVANCED
- Backup, Recovery und Migration
- Performance-Optimierung: Query-Tuning und Caching
- Produktive Systeme: Monitoring und Skalierung
ABSCHLUSS
Was ist MariaDB
MariaDB ist eine Datenbank für strukturierte Daten. Stell Dir vor: Ein Excel-Sheet, aber richtig robust, schnell und für Millionen von Einträgen geeignet. MariaDB speichert Daten in Tabellen mit Zeilen und Spalten und sorgt dafür, dass alles konsistent bleibt.
Im KI-System übernimmt MariaDB die verlässliche Datenhaltung:
- User-Accounts und Authentifizierung
- Transaktionen und Bestellungen
- Logs und Performance-Metriken
- Konfigurationen und Einstellungen
Während Ollama Texte generiert und ChromaDB semantisch sucht, hält MariaDB die strukturierten Daten zusammen. Das macht es zum Rückgrat des gesamten KI-Stacks.
MariaDB ist eine Datenbank für strukturierte Daten. Stell Dir vor: Ein Excel-Sheet, aber richtig robust, schnell und für Millionen von Einträgen geeignet. MariaDB speichert Daten in Tabellen mit Zeilen und Spalten und sorgt dafür, dass alles konsistent bleibt.
Im KI-System übernimmt MariaDB die verlässliche Datenhaltung:
- User-Accounts und Authentifizierung
- Transaktionen und Bestellungen
- Logs und Performance-Metriken
- Konfigurationen und Einstellungen
Während Ollama Texte generiert und ChromaDB semantisch sucht, hält MariaDB die strukturierten Daten zusammen. Das macht es zum Rückgrat des gesamten KI-Stacks.
Der Vorteil: ACID
MariaDB garantiert, dass Daten nicht verloren gehen, konsistent bleiben und bei Fehlern zurückgerollt werden können. Das ist entscheidend für geschäftskritische Daten wie User-Accounts oder Transaktionen.
MariaDB: Ein Fork von MySQL
MariaDB ist ein relationaler Datenbank-Server, der 2009 als Fork von MySQL entstand, nachdem Oracle MySQL übernommen hatte. Die Entwicklung wird von der MariaDB Foundation geleitet, was die Open-Source-Ausrichtung sichert.
Aus meiner Perspektive ist MariaDB die bessere Wahl als MySQL geworden. Die Entwicklung ist transparenter, neue Features kommen schneller und die Lizenzierung ist klarer (GPL).
Kernmerkmale
- Relationales Modell: Daten in Tabellen mit definierten Beziehungen
- ACID-Eigenschaften: Atomicity, Consistency, Isolation, Durability
- SQL-Standard: Strukturierte Abfragesprache (SQL)
- Storage Engines: InnoDB (default), Aria, MyISAM
- Transaktionen: Mehrere Operationen atomar ausführen
MariaDB im KI-Stack
In einem KI-System arbeiten drei Datenbanken zusammen:
| Komponente | Aufgabe | Beispiel |
|---|---|---|
| MariaDB | Strukturierte Daten, Relationen | users, sessions, transactions |
| ChromaDB | Semantische Suche | documents, knowledge_base |
| Ollama | LLM-Inferenz | chat, summaries, generation |
Versionen und Kompatibilität
Aktuelle stabile Version: MariaDB 11.x (November 2024) Langzeit-Support: MariaDB 10.11 LTS (bis 2028)
MySQL-Kompatibilität: MariaDB ist weitgehend MySQL-kompatibel. Die meisten MySQL-Anwendungen laufen ohne Änderungen. Aber: Ab MariaDB 10.6 gibt es Unterschiede in den Defaults und Features.
Aus meiner Erfahrung
Ich setze MariaDB seit über 10 Jahren produktiv ein. Die Stabilität ist hervorragend, die Performance für 99% der Anwendungsfälle völlig ausreichend. Für KI-Systeme ist die Kombination mit ChromaDB (Vektoren) und Ollama (LLMs) besonders mächtig: Strukturierte Daten bleiben in MariaDB, semantische Suche läuft über ChromaDB.
SQL-Datenbanken vs. Vektordatenbanken
SQL-Datenbanken und Vektordatenbanken lösen unterschiedliche Probleme. Beide sind wichtig im KI-Stack, aber für verschiedene Aufgaben.
SQL-Datenbanken und Vektordatenbanken lösen unterschiedliche Probleme. Beide sind wichtig im KI-Stack, aber für verschiedene Aufgaben.
SQL-Datenbanken (MariaDB)
Strukturierte Daten mit klaren Beziehungen. Du fragst: "Zeige mir alle Bestellungen von User 123" und bekommst exakt das.
Stärke: Präzise, verlässlich, strukturiert.
Vektordatenbanken (ChromaDB)
Semantische Suche in Texten. Du fragst: "Finde Dokumente über Quantencomputing" und bekommst ähnliche Inhalte, auch wenn das Wort nicht genau vorkommt.
Stärke: Bedeutung verstehen, Ähnlichkeit finden.
Wann was?
MariaDB: User-Daten, Transaktionen, Logs, alles Strukturierte. ChromaDB: Dokumente, Texte, Knowledge-Base, alles Semantische.
Fundamentale Unterschiede
SQL-Datenbanken und Vektordatenbanken basieren auf völlig unterschiedlichen Paradigmen. Beide haben ihre Berechtigung im modernen KI-Stack.
Datenmodell und Abfragen
| Aspekt | SQL (MariaDB) | Vektoren (ChromaDB) |
|---|---|---|
| Datenstruktur | Tabellen mit Spalten (Schema) | Collections mit Embeddings (schemalos) |
| Suche | Exakte Matches (WHERE email = '...') | Semantische Ähnlichkeit (Cosine Distance) |
| Beziehungen | Foreign Keys, JOINs | Metadaten-Referenzen |
| Integrität | ACID-Garantien | Eventual Consistency |
| Typische Queries | SELECT, JOIN, GROUP BY | query(), n_results, where |
Konkrete Beispiele
SQL-Abfrage (MariaDB)
-- Finde alle Bestellungen von User "max@example.com"
SELECT o.id, o.amount, o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.email = 'max@example.com'
ORDER BY o.created_at DESC;
Ergebnis: Exakt die Bestellungen dieses Users, strukturiert und verlässlich.
Vektor-Abfrage (ChromaDB)
# Finde Dokumente ähnlich zu "Wie funktioniert Quantencomputing?"
results = collection.query(
query_texts=["Wie funktioniert Quantencomputing?"],
n_results=5
)
# Ergebnis: Dokumente mit semantisch ähnlichem Inhalt,
# auch wenn "Quantencomputing" nicht wörtlich vorkommt
Häufiger Fehler: Falsche Datenbank für den Use-Case
Nicht gut: User-Accounts in ChromaDB speichern (keine Relationen, keine ACID) Nicht gut: Gesamte Dokumentation in MariaDB als TEXT-Felder (keine semantische Suche)
Besser: User-Daten in MariaDB, Dokumentations-Embeddings in ChromaDB, beide verknüpft über IDs.
Aus meiner Erfahrung
Die Kombination macht es aus. Ich nutze MariaDB für alles Strukturierte (User, Permissions, Logs), ChromaDB für alles Semantische (Dokumente, Code-Snippets). Die Referenz zwischen beiden läuft über IDs, die ich in MariaDB speichere und als Metadaten in ChromaDB hinterlege.
Das Spannende: Ein RAG-System braucht beide. MariaDB für den User-Context (Wer fragt? Welche Permissions?), ChromaDB für relevante Dokumente (Was ist ähnlich?), Ollama für die Antwort.
Kernkonzepte: Tabellen, Relationen, Transaktionen
MariaDB organisiert Daten in drei zentralen Konzepten: Tabellen, Relationen und Transaktionen. Das klingt technisch, ist aber eigentlich ganz logisch.
MariaDB organisiert Daten in drei zentralen Konzepten: Tabellen, Relationen und Transaktionen. Das klingt technisch, ist aber eigentlich ganz logisch.
Tabellen: Daten strukturiert ablegen
Eine Tabelle ist wie eine Excel-Tabelle: Spalten definieren, was gespeichert wird (Name, Email, Alter), Zeilen enthalten die eigentlichen Daten. Jede Zeile ist ein Datensatz.
Relationen: Daten sinnvoll verknüpfen
Statt alles in eine riesige Tabelle zu packen, verknüpfst Du mehrere Tabellen. Beispiel: Eine Tabelle für User, eine für Bestellungen. Die Bestellung verweist auf den User. So vermeidest Du Redundanz.
Transaktionen: Alles oder nichts
Eine Transaktion gruppiert mehrere Operationen. Entweder alle funktionieren oder keine. Beispiel: Geld von Konto A auf Konto B überweisen. Wenn das Abbuchen klappt, aber das Gutschreiben fehlschlägt, wird alles rückgängig gemacht.
Tabellen im Detail
Eine Tabelle besteht aus Spalten (Columns) und Zeilen (Rows). Jede Spalte hat einen Datentyp, der definiert, was gespeichert werden kann.
Beispiel: Users-Tabelle
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Wichtige Elemente:
PRIMARY KEY: Eindeutiger Identifier (ID)AUTO_INCREMENT: Automatische NummernvergabeNOT NULL: Feld muss gefüllt seinUNIQUE: Wert darf nur einmal vorkommenINDEX: Beschleunigt Suchen auf dieser SpalteInnoDB: Storage Engine (unterstützt Transaktionen)
Datentypen
| Typ | Verwendung | Beispiel |
|---|---|---|
INT |
Ganzzahlen | user_id, age, count |
VARCHAR(n) |
Texte variabler Länge | email, name (max n Zeichen) |
TEXT |
Lange Texte | description, notes |
TIMESTAMP |
Zeitstempel | created_at, updated_at |
BOOLEAN |
Wahr/Falsch | is_active, verified |
Relationen: 1:n und n:m
Relationen verknüpfen Tabellen logisch. Es gibt drei Haupttypen:
1:n Beziehung (One-to-Many)
Ein User hat viele Bestellungen:
-- Users (1)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL
);
-- Orders (n)
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
amount DECIMAL(10,2),
FOREIGN KEY (user_id) REFERENCES users(id)
);
n:m Beziehung (Many-to-Many)
Viele User können viele Kurse haben (über Zwischentabelle):
-- Users
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));
-- Courses
CREATE TABLE courses (id INT PRIMARY KEY, title VARCHAR(200));
-- Zwischentabelle (Junction Table)
CREATE TABLE user_courses (
user_id INT,
course_id INT,
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, course_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
Transaktionen: ACID-Eigenschaften
Transaktionen garantieren Datenkonsistenz. Die vier ACID-Eigenschaften:
- Atomicity (Atomarität): Alles oder nichts. Entweder alle Operationen erfolgreich oder keine.
- Consistency (Konsistenz): Daten bleiben gültig (Foreign Keys, Constraints werden eingehalten).
- Isolation (Isolation): Parallele Transaktionen stören sich nicht gegenseitig.
- Durability (Dauerhaftigkeit): Committed Daten bleiben erhalten, auch bei Crash.
Praktisches Beispiel:
START TRANSACTION;
-- Geld abbuchen
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- Geld gutschreiben
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
-- Beide erfolgreich? Dann commit
COMMIT;
-- Fehler passiert? Alles rückgängig
-- ROLLBACK;
Aus meiner Perspektive
Transaktionen sind der Grund, warum ich für kritische Daten immer MariaDB nutze. ChromaDB hat keine Transaktionen (ist auch nicht nötig für Vektoren). Aber für User-Accounts, Zahlungen, Permissions ist ACID unverzichtbar.
Ich habe zu viele Systeme gesehen, die "einfach" alles in JSON-Files oder NoSQL speichern und dann bei Concurrency-Problemen scheitern. MariaDB verhindert das durch Design.
Warum MariaDB im KI-Stack
Warum ausgerechnet MariaDB? Es gibt viele Datenbanken, aber MariaDB bietet eine besonders gute Balance für KI-Systeme.
Warum ausgerechnet MariaDB? Es gibt viele Datenbanken, aber MariaDB bietet eine besonders gute Balance für KI-Systeme.
Die wichtigsten Gründe
- Open Source: Keine Lizenzkosten, volle Kontrolle
- Stabil: Seit Jahren bewährt in Production
- Schnell: Für die meisten Anwendungen völlig ausreichend
- Kompatibel: Funktioniert mit PHP, Python und fast allen Frameworks
- ACID: Daten bleiben konsistent, auch bei Problemen
Im KI-Stack brauchst Du eine Datenbank, die verlässlich ist. Während Ollama Texte generiert und ChromaDB semantisch sucht, hält MariaDB die kritischen strukturierten Daten zusammen: User-Accounts, Berechtigungen, Logs, Metriken.
Alternativen
PostgreSQL ist technisch mächtiger, aber komplexer. MySQL ist ähnlich, aber unter Oracle-Kontrolle. SQLite ist einfacher, aber nicht für Multi-User geeignet. MariaDB trifft den Sweet Spot.
MariaDB vs. Alternativen
Ich nutze MariaDB seit der ersten Version (2009) produktiv. Die Entscheidung war damals von MySQL wegzugehen, und ich bereue sie nicht.
Vergleich: MariaDB vs. MySQL vs. PostgreSQL
| Aspekt | MariaDB | MySQL | PostgreSQL |
|---|---|---|---|
| Lizenz | GPL (Open Source) | Dual (GPL + Commercial) | PostgreSQL License (BSD-ähnlich) |
| Performance | Sehr gut (Read-Heavy) | Sehr gut (Read-Heavy) | Exzellent (Write-Heavy) |
| Features | Standard + Extras (JSON, Sequences) | Standard | Umfangreich (JSONB, Arrays, CTEs) |
| Komplexität | Mittel | Niedrig | Hoch |
| Community | Groß, aktiv | Sehr groß | Groß, technisch versiert |
Spezifische Vorteile für KI-Systeme
1. JSON-Support
MariaDB unterstützt JSON-Spalten. Das ist praktisch für KI-Metadaten:
CREATE TABLE llm_responses (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
response TEXT,
metadata JSON, -- Flexibel für Model, Tokens, etc.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Query
SELECT * FROM llm_responses
WHERE JSON_EXTRACT(metadata, '$.model') = 'llama3.2:3b';
2. Sequences (seit 10.3)
Ähnlich wie PostgreSQL SERIAL, aber flexibler:
CREATE SEQUENCE user_id_seq START WITH 1000;
CREATE TABLE users (
id INT DEFAULT NEXTVAL(user_id_seq) PRIMARY KEY,
email VARCHAR(255)
);
3. Storage Engines
MariaDB bietet verschiedene Storage Engines für unterschiedliche Anforderungen:
- InnoDB: Default, ACID, Foreign Keys (für kritische Daten)
- Aria: Crash-Safe, für temporäre Daten
- MyISAM: Schnell für Read-Only (veraltet, nicht empfohlen)
Performance-Charakteristik
Aus meiner Erfahrung mit produktiven Systemen:
- SELECT-Queries: Bis 10.000 Queries/s auf Standard-Hardware (mit Indizes)
- INSERT-Throughput: Circa 5.000-15.000 Inserts/s (abhängig von Indizes)
- JOIN-Performance: Exzellent mit richtigen Indizes, problematisch ohne
- Connection-Handling: Bis 500 parallele Connections (Standard-Config)
Grenzen von MariaDB
MariaDB ist nicht perfekt für alles:
- Keine semantische Suche: Dafür ist ChromaDB besser
- Keine horizontale Skalierung: Replikation ja, Sharding kompliziert
- Schema-gebunden: Flexibilität kostet (Migrations nötig)
Das ist OK. Jedes Tool hat seinen Platz. MariaDB macht strukturierte Daten perfekt, ChromaDB macht Vektoren perfekt, zusammen sind sie stark.
Aus meiner Perspektive
Ich habe mit PostgreSQL, MySQL, MariaDB und SQLite produktiv gearbeitet. Für den KI-Stack wähle ich MariaDB, weil:
- Setup einfach (simpler als PostgreSQL)
- Performance ausreichend (für 95% der Use-Cases)
- Community groß (viele Ressourcen, Lösungen verfügbar)
- JSON-Support vorhanden (für flexible KI-Metadaten)
- Open Source ohne Hintergedanken (nicht Oracle-kontrolliert)
Für ein RAG-System mit Ollama und ChromaDB ist MariaDB die solide Basis für User-Verwaltung, Logging und Metriken. Es macht einfach, was es soll.
MariaDB + Ollama + ChromaDB: Das Zusammenspiel
MariaDB, Ollama und ChromaDB arbeiten zusammen wie ein Team. Jede Komponente hat ihre Aufgabe, zusammen bilden sie ein vollständiges KI-System.
MariaDB, Ollama und ChromaDB arbeiten zusammen wie ein Team. Jede Komponente hat ihre Aufgabe, zusammen bilden sie ein vollständiges KI-System.
Das Zusammenspiel am Beispiel: Chat-System
1. User fragt etwas MariaDB prüft: Ist der User eingeloggt? Welche Permissions hat er?
2. Relevante Dokumente finden ChromaDB sucht semantisch ähnliche Inhalte aus der Knowledge-Base.
3. Antwort generieren Ollama bekommt die Frage + gefundene Dokumente und generiert eine Antwort.
4. Alles loggen MariaDB speichert: Wer fragte wann? Welches Modell? Wie lange dauerte es?
Jede Komponente macht das, was sie am besten kann. Das Ergebnis ist ein System, das zuverlässig, semantisch und intelligent zugleich ist.
Architektur eines RAG-Systems
Ein RAG-System (Retrieval-Augmented Generation) kombiniert alle drei Komponenten. Ich zeige das an einem konkreten Beispiel aus der Praxis.
Use-Case: Dokumentations-Chatbot
Anforderung: User sollen Fragen zur Produktdokumentation stellen können.
Schritt 1: Authentifizierung (MariaDB)
-- User-Session prüfen
SELECT u.id, u.email, u.access_level
FROM users u
JOIN sessions s ON u.id = s.user_id
WHERE s.session_token = ? AND s.expires_at > NOW();
Schritt 2: Relevante Dokumente finden (ChromaDB)
# Python
collection = chroma_client.get_collection("documentation")
results = collection.query(
query_texts=["Wie installiere ich Ollama?"],
n_results=3,
where={"user_access_level": {"$lte": user_access_level}}
)
# Ergebnis: 3 ähnlichste Dokumente mit Access-Check
Schritt 3: Antwort generieren (Ollama)
# Python
context = "\n\n".join(results['documents'][0])
prompt = f"""Beantworte diese Frage basierend auf dem Kontext:
Kontext:
{context}
Frage: Wie installiere ich Ollama?
Antwort:"""
response = ollama.generate(model="llama3.2:3b", prompt=prompt)
Schritt 4: Konversation loggen (MariaDB)
-- Log-Eintrag erstellen
INSERT INTO chat_log (
user_id, session_id, question, answer,
model, tokens_generated, duration_ms, created_at
) VALUES (
?, ?, ?, ?,
'llama3.2:3b', 245, 2340, NOW()
);
Datenfluss visualisiert
User-Request
↓
┌─────────────────┐
│ PHP/Python │ ← API-Layer
└────────┬────────┘
│
├──→ [MariaDB] Auth + Session-Check
│ ↓
│ User-ID + Access-Level
│ ↓
├──→ [ChromaDB] Similarity Search
│ ↓
│ Relevante Dokumente
│ ↓
├──→ [Ollama] Text-Generierung
│ ↓
│ Antwort-Text
│ ↓
└──→ [MariaDB] Logging + Metriken
↓
Response an User
Aus meiner Erfahrung
Das Zusammenspiel funktioniert am besten, wenn jede Komponente ihre Kernaufgabe macht:
- MariaDB: Strukturierte Daten (User, Permissions, Logs) - ACID ist hier entscheidend
- ChromaDB: Content-Retrieval (Dokumente, Code) - Semantik ist hier entscheidend
- Ollama: Textgenerierung (Antworten, Summaries) - Lokale Inferenz ist hier entscheidend
Ich habe Systeme gesehen, die versuchen, alles in einer Datenbank zu machen. Das funktioniert nie optimal. Die Aufgabenteilung ist der Schlüssel.
Code-Beispiel: Vollständiger Workflow (PHP)
// 1. User authentifizieren (MariaDB)
$stmt = $pdo->prepare("SELECT id, access_level FROM users WHERE session_token = ?");
$stmt->execute([$sessionToken]);
$user = $stmt->fetch();
// 2. Relevante Docs finden (ChromaDB via Python-Script)
$chromaResults = exec("python3 /path/to/chroma_search.py " . escapeshellarg($question));
$docs = json_decode($chromaResults, true);
// 3. Ollama-Antwort generieren
$context = implode("\n\n", $docs['documents']);
$ollamaPrompt = "Kontext: $context\n\nFrage: $question\n\nAntwort:";
$ch = curl_init('http://localhost:11434/api/generate');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'model' => 'llama3.2:3b',
'prompt' => $ollamaPrompt
]));
$response = json_decode(curl_exec($ch), true);
// 4. Alles loggen (MariaDB)
$stmt = $pdo->prepare("
INSERT INTO chat_log (user_id, question, answer, model, tokens, created_at)
VALUES (?, ?, ?, ?, ?, NOW())
");
$stmt->execute([
$user['id'],
$question,
$response['response'],
'llama3.2:3b',
$response['eval_count']
]);
Das ist ein vollständiges RAG-System in circa 30 Zeilen PHP. Einfach, funktionierend, wartbar.
Prepared Statements: Sicherer Datenbankzugriff
Prepared Statements sind die sichere Art, Daten in die Datenbank zu schreiben. Sie verhindern SQL-Injection, eine der gefährlichsten Sicherheitslücken im Web.
Prepared Statements sind die sichere Art, Daten in die Datenbank zu schreiben. Sie verhindern SQL-Injection, eine der gefährlichsten Sicherheitslücken im Web.
Das Problem: SQL-Injection
Wenn Du User-Eingaben direkt in SQL-Queries einbaust, kann ein Angreifer Deine Datenbank übernehmen oder löschen.
Gefährlich:
"SELECT * FROM users WHERE email = '" . $_POST['email'] . "'"
Ein Angreifer gibt ein: ' OR '1'='1
Resultat: Alle User werden zurückgegeben!
Die Lösung: Prepared Statements
Du trennst die Query-Struktur von den Daten. Die Datenbank weiß: "Das ist Code, das sind Daten" und vermischt beides niemals.
Sicher:
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?"); $stmt->execute([$email]);
Nutze immer Prepared Statements, wenn User-Daten involviert sind. Keine Ausnahmen.
Wie Prepared Statements funktionieren
Prepared Statements trennen die Query-Definition von den Daten. Der Ablauf:
- Prepare: Query mit Platzhaltern definieren
- Bind: Werte an Platzhalter binden (optional explizit)
- Execute: Query mit gebundenen Werten ausführen
Syntax: Fragezeichen vs. Named Parameters
Positionale Parameter (Fragezeichen)
// PHP (PDO)
$stmt = $pdo->prepare("
INSERT INTO users (email, name, created_at)
VALUES (?, ?, NOW())
");
$stmt->execute([$email, $name]);
// Python (pymysql)
cursor.execute(
"INSERT INTO users (email, name) VALUES (%s, %s)",
(email, name)
)
Named Parameters (benannt)
// PHP (PDO)
$stmt = $pdo->prepare("
INSERT INTO users (email, name, created_at)
VALUES (:email, :name, NOW())
");
$stmt->execute([
'email' => $email,
'name' => $name
]);
// Vorteil: Lesbarer bei vielen Parametern
Aus meiner Perspektive
Ich nutze Named Parameters bei komplexen Queries (> 5 Parameter), Fragezeichen bei einfachen. Named sind selbstdokumentierend, Fragezeichen sind kürzer.
SQL-Injection: Konkrete Angriffe
Um zu verstehen, warum Prepared Statements wichtig sind, schauen wir uns echte Angriffe an.
Angriff 1: Authentication Bypass
// UNSICHER
$query = "SELECT * FROM users WHERE email = '{$_POST['email']}' AND password = '{$_POST['password']}'";
// Angreifer gibt ein:
// Email: admin@example.com
// Password: ' OR '1'='1
// Resultat-Query:
// SELECT * FROM users WHERE email = 'admin@example.com' AND password = '' OR '1'='1'
// → Liefert alle Users (weil '1'='1' immer wahr ist)
Angriff 2: Daten-Exfiltration
// UNSICHER
$query = "SELECT * FROM products WHERE id = {$_GET['id']}";
// Angreifer gibt ein:
// id = 1 UNION SELECT email, password, NULL FROM users
// Resultat: Alle User-Credentials werden ausgegeben
Schutz durch Prepared Statements
// SICHER
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND password = ?");
$stmt->execute([$email, $password]);
// Angreifer gibt ein:
// Email: admin@example.com
// Password: ' OR '1'='1
// Was passiert: Die Query sucht nach einem User mit
// Email = "admin@example.com" UND Password = "' OR '1'='1"
// (als String, nicht als SQL-Code!)
// → Findet nichts, Angriff fehlgeschlagen
Was Prepared Statements NICHT schützen
- Tabellen-/Spaltennamen: Können nicht parametrisiert werden
- ORDER BY Direction: ASC/DESC kann nicht gebunden werden
- LIMIT-Werte: Müssen separat validiert werden
Lösung: Whitelist für dynamische Teile verwenden
// Spaltenname aus Whitelist
$allowedColumns = ['name', 'email', 'created_at'];
$column = in_array($_GET['sort'], $allowedColumns) ? $_GET['sort'] : 'id';
$stmt = $pdo->prepare("SELECT * FROM users ORDER BY $column DESC");
Performance-Vorteil von Prepared Statements
Neben der Sicherheit bringen Prepared Statements auch Performance-Vorteile:
- Query wird einmal geparst und gecacht
- Bei wiederholter Ausführung (loop) nur Parameter ändern
- MariaDB optimiert den Execution-Plan
Benchmark-Beispiel:
// 1000 Inserts ohne Prepared Statement: ~2.5 Sekunden
for ($i = 0; $i < 1000; $i++) {
$pdo->query("INSERT INTO test (value) VALUES ($i)");
}
// 1000 Inserts mit Prepared Statement: ~0.8 Sekunden
$stmt = $pdo->prepare("INSERT INTO test (value) VALUES (?)");
for ($i = 0; $i < 1000; $i++) {
$stmt->execute([$i]);
}
// 3x schneller!
Aus meiner Erfahrung
Ich nutze zu 100% Prepared Statements, keine Ausnahmen. Selbst bei "vertrauenswürdigen" Daten (z.B. aus eigenen Scripts). Warum? Weil es zur Gewohnheit werden muss. Sobald man Ausnahmen macht, schleichen sich Fehler ein.
In KI-Systemen kommen Daten aus vielen Quellen: User-Input, Ollama-Output, ChromaDB-Metadaten. Alles kann potentiell SQL-Code enthalten. Prepared Statements machen das irrelevant.
Best Practices
- IMMER Prepared Statements für User-Input
- IMMER Prepared Statements für externe Daten
- NIEMALS String-Konkatenation in SQL-Queries
- Whitelist für dynamische Spalten-/Tabellennamen
- Input-Validierung zusätzlich (Defense in Depth)
MariaDB mit PHP und Python
Im KI-Stack nutzt Du oft zwei Sprachen: PHP für das Web-Backend, Python für KI-Processing. Beide können mit MariaDB arbeiten.
Im KI-Stack nutzt Du oft zwei Sprachen: PHP für das Web-Backend, Python für KI-Processing. Beide können mit MariaDB arbeiten.
PHP: Das Web-Backend
PHP läuft auf dem Server und liefert Webseiten aus. Es greift auf MariaDB zu für User-Daten, Sessions, Logs. Die Standard-Bibliothek heißt PDO.
Typisch für: Web-Frontends, REST-APIs, User-Verwaltung
Python: Die KI-Pipeline
Python läuft oft als Script oder Service und verarbeitet Daten. Es nutzt Ollama und ChromaDB direkt, speichert Ergebnisse in MariaDB. Die Bibliothek heißt pymysql oder SQLAlchemy.
Typisch für: Batch-Jobs, Embedding-Generierung, Daten-Processing
Beide Sprachen greifen auf dieselbe MariaDB zu. PHP für Web-Anfragen, Python für KI-Processing. Das funktioniert problemlos, solange beide Prepared Statements nutzen.
PHP: PDO (PHP Data Objects)
PDO ist die moderne Datenbank-Abstraktion in PHP. Sie unterstützt MySQL, MariaDB, PostgreSQL, SQLite und mehr über eine einheitliche API.
Verbindung aufbauen
// Verbindung mit Fehlerbehandlung
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=ki_system;charset=utf8mb4',
'username',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
error_log("DB Error: " . $e->getMessage());
die("Database connection failed");
}
CRUD-Operationen (PHP/PDO)
// CREATE
$stmt = $pdo->prepare("INSERT INTO users (email, name) VALUES (?, ?)");
$stmt->execute([$email, $name]);
$userId = $pdo->lastInsertId();
// READ
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch(); // Ein Datensatz
// oder
$users = $stmt->fetchAll(); // Alle Datensätze
// UPDATE
$stmt = $pdo->prepare("UPDATE users SET name = ? WHERE id = ?");
$stmt->execute([$newName, $userId]);
$affected = $stmt->rowCount();
// DELETE
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$userId]);
$deleted = $stmt->rowCount();
Python: pymysql und SQLAlchemy
Python hat zwei gängige Wege, mit MariaDB zu arbeiten:
1. pymysql (Low-Level, direkt)
import pymysql
# Verbindung
connection = pymysql.connect(
host='localhost',
user='username',
password='password',
database='ki_system',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
try:
with connection.cursor() as cursor:
# INSERT
cursor.execute(
"INSERT INTO users (email, name) VALUES (%s, %s)",
('user@example.com', 'Max')
)
connection.commit()
with connection.cursor() as cursor:
# SELECT
cursor.execute("SELECT * FROM users WHERE email = %s", ('user@example.com',))
user = cursor.fetchone()
print(user['name'])
finally:
connection.close()
2. SQLAlchemy (ORM, abstrahiert)
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Engine
engine = create_engine('mysql+pymysql://user:pass@localhost/ki_system')
Session = sessionmaker(bind=engine)
Base = declarative_base()
# Model definieren
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String(255), unique=True, nullable=False)
name = Column(String(100))
# Nutzen
session = Session()
# INSERT
new_user = User(email='user@example.com', name='Max')
session.add(new_user)
session.commit()
# SELECT
user = session.query(User).filter_by(email='user@example.com').first()
print(user.name)
session.close()
pymysql vs. SQLAlchemy: Wann was?
| Aspekt | pymysql | SQLAlchemy |
|---|---|---|
| Komplexität | Niedrig | Hoch |
| Kontrolle | Vollständig (rohe SQL) | Abstrahiert (ORM) |
| Performance | Schneller (weniger Overhead) | Langsamer (Overhead durch ORM) |
| Use-Case | Scripts, einfache Queries | Komplexe Apps, Migrations |
Praxis-Beispiel: PHP + Python zusammen
Ein typischer Workflow im KI-System:
PHP: Web-Request verarbeiten
// user_question.php
$stmt = $pdo->prepare("
INSERT INTO chat_requests (user_id, question, status)
VALUES (?, ?, 'pending')
");
$stmt->execute([$userId, $question]);
$requestId = $pdo->lastInsertId();
// Python-Script triggern (async)
exec("python3 /path/to/process_question.py $requestId > /dev/null 2>&1 &");
echo json_encode(['request_id' => $requestId, 'status' => 'processing']);
Python: KI-Processing
# process_question.py
import sys
import pymysql
import chromadb
import ollama
request_id = sys.argv[1]
# 1. Request aus MariaDB laden
conn = pymysql.connect(host='localhost', database='ki_system', user='...', password='...')
cursor = conn.cursor()
cursor.execute("SELECT user_id, question FROM chat_requests WHERE id = %s", (request_id,))
request = cursor.fetchone()
# 2. Relevante Docs aus ChromaDB
chroma_client = chromadb.Client()
collection = chroma_client.get_collection("docs")
results = collection.query(query_texts=[request['question']], n_results=3)
# 3. Ollama-Antwort generieren
context = "\n".join(results['documents'][0])
response = ollama.generate(
model='llama3.2:3b',
prompt=f"Context: {context}\n\nFrage: {request['question']}\n\nAntwort:"
)
# 4. Ergebnis in MariaDB speichern
cursor.execute(
"UPDATE chat_requests SET answer = %s, status = 'completed' WHERE id = %s",
(response['response'], request_id)
)
conn.commit()
conn.close()
Aus meiner Perspektive
Ich nutze PHP für alles Web-Facing (Frontend, REST-API, User-Management). Python für alles KI-Processing (Embeddings, Batch-Jobs, Complex-Queries mit Pandas). Beide sprechen MariaDB, beide nutzen Prepared Statements.
Das Spannende: Du kannst PHP und Python parallel laufen lassen. PHP bedient User-Requests schnell, Python verarbeitet im Hintergrund. MariaDB ist die gemeinsame Datenbasis.
Installation und Setup
MariaDB zu installieren ist einfach. Die meisten Linux-Distributionen haben es in ihren Paketquellen, auf macOS und Windows gibt es Installer.
MariaDB zu installieren ist einfach. Die meisten Linux-Distributionen haben es in ihren Paketquellen, auf macOS und Windows gibt es Installer.
Linux (Ubuntu/Debian)
sudo apt update
sudo apt install mariadb-server
sudo systemctl start mariadb
sudo mysql_secure_installation
Das wars. Nach ein paar Minuten läuft MariaDB.
macOS
brew install mariadb
brew services start mariadb
mysql_secure_installation
Erster Test
Nach der Installation kannst Du Dich verbinden:
mysql -u root -p
Wenn Du eine Shell siehst, funktioniert es.
Plattformspezifische Installation
MariaDB unterstützt alle gängigen Betriebssysteme. Ich zeige die Installation für Production-Setups.
Linux Installation (Ubuntu 22.04/24.04)
Schritt 1: Installation
# Pakete aktualisieren
sudo apt update
sudo apt upgrade -y
# MariaDB installieren
sudo apt install mariadb-server mariadb-client -y
# Version prüfen
mariadb --version
# Erwartete Ausgabe: mariadb Ver 15.1 Distrib 10.11.x-MariaDB
Schritt 2: Sicherheit härten
# Sicherheits-Script ausführen
sudo mysql_secure_installation
# Fragen die gestellt werden:
# - Root-Password setzen? → JA
# - Anonymous User entfernen? → JA
# - Root-Remote-Login verbieten? → JA
# - Test-Datenbank löschen? → JA
# - Privileges neu laden? → JA
Schritt 3: User und Datenbank erstellen
# Als root einloggen
sudo mysql -u root -p
-- Datenbank erstellen
CREATE DATABASE ki_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- User erstellen
CREATE USER 'ki_user'@'localhost' IDENTIFIED BY 'sicheres_password_hier';
-- Rechte vergeben
GRANT ALL PRIVILEGES ON ki_system.* TO 'ki_user'@'localhost';
FLUSH PRIVILEGES;
-- Testen
EXIT;
-- Als neuer User einloggen
mysql -u ki_user -p ki_system
Wichtig: Zeichensatz UTF8MB4
Nutze immer UTF8MB4, nicht UTF8. UTF8 in MySQL/MariaDB ist unvollständig (max 3 Bytes). UTF8MB4 unterstützt alle Unicode-Zeichen inkl. Emojis (4 Bytes).
-- Richtig
CREATE DATABASE ki_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Falsch (veraltet)
CREATE DATABASE ki_system CHARACTER SET utf8;
macOS Installation (via Homebrew)
# Installation
brew install mariadb
# Service starten
brew services start mariadb
# Oder manuell (ohne Service):
mysql.server start
# Sicherheit
mysql_secure_installation
# User erstellen (wie bei Linux)
mysql -u root -p
-- dann SQL-Befehle wie oben
Docker Installation (für Development)
# Einfachste Variante
docker run -d \
--name mariadb \
-e MYSQL_ROOT_PASSWORD=root_password \
-e MYSQL_DATABASE=ki_system \
-e MYSQL_USER=ki_user \
-e MYSQL_PASSWORD=user_password \
-p 3306:3306 \
mariadb:11
# Mit Volume (Daten bleiben erhalten)
docker run -d \
--name mariadb \
-e MYSQL_ROOT_PASSWORD=root_password \
-e MYSQL_DATABASE=ki_system \
-e MYSQL_USER=ki_user \
-e MYSQL_PASSWORD=user_password \
-v mariadb-data:/var/lib/mysql \
-p 3306:3306 \
mariadb:11
# Verbinden
docker exec -it mariadb mysql -u ki_user -p ki_system
Konfiguration für KI-Workloads
Für KI-Systeme mit vielen Inserts (Logs, Metriken) sind ein paar Tuning-Parameter sinnvoll:
# /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
# InnoDB Buffer Pool (nutze 60-70% vom RAM)
innodb_buffer_pool_size = 2G
# Connection-Limit erhöhen
max_connections = 500
# Binär-Logs für Replikation (optional)
log_bin = /var/log/mysql/mariadb-bin
expire_logs_days = 7
# Query-Cache (deprecated in 10.x, aber nützlich)
# Für 10.x: Besser Application-Level-Caching
# Slow-Query-Log (für Debugging)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# Nach Änderungen: Neustart
sudo systemctl restart mariadb
Aus meiner Erfahrung
Für Development nutze ich Docker (schnell, isoliert, wegwerfbar). Für Production native Installation auf Linux (stabiler, besser zu monitoren).
Das innodb_buffer_pool_size ist der wichtigste Tuning-Parameter. Zu klein = langsam. Zu groß = System swappt. Sweet Spot: 60-70% vom verfügbaren RAM.
Verifikation der Installation
# Version prüfen
mariadb --version
# Service-Status (Linux)
sudo systemctl status mariadb
# In MariaDB einloggen
sudo mysql -u root -p
# Datenbanken anzeigen
SHOW DATABASES;
# User anzeigen
SELECT User, Host FROM mysql.user;
# Prozesse anzeigen
SHOW PROCESSLIST;
Datenbanken und Tabellen erstellen
Eine Datenbank erstellen ist simpel. Du definierst einen Namen, einen Zeichensatz und legst los. Tabellen definieren dann, welche Daten Du speichern willst.
Eine Datenbank erstellen ist simpel. Du definierst einen Namen, einen Zeichensatz und legst los. Tabellen definieren dann, welche Daten Du speichern willst.
Datenbank erstellen
CREATE DATABASE mein_ki_system;
Das erstellt eine leere Datenbank. Darin kannst Du dann Tabellen anlegen.
Tabelle erstellen
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
name VARCHAR(100)
);
Das erstellt eine Tabelle mit 3 Spalten: ID (automatisch), Email (Pflichtfeld) und Name.
Das Prinzip: Erst Datenbank, dann Tabellen darin. Jede Tabelle hat Spalten mit Datentypen.
Datenbank-Design für KI-Systeme
Gutes Schema-Design ist entscheidend. Ich zeige das an einem konkreten KI-System.
Datenbank erstellen mit allen Optionen
CREATE DATABASE ki_system
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
Wichtig:
utf8mb4: Vollständiger Unicode (inkl. Emojis)utf8mb4_unicode_ci: Case-Insensitive Sortierung
Beispiel-Schema: Chat-System mit KI
Tabelle 1: Users
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
access_level TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_access_level (access_level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Tabelle 2: Sessions
CREATE TABLE sessions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
session_token VARCHAR(64) NOT NULL UNIQUE,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (session_token),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB;
Tabelle 3: Chat-Logs
CREATE TABLE chat_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
session_id INT,
question TEXT NOT NULL,
answer TEXT,
model VARCHAR(50),
tokens_generated INT,
duration_ms INT,
metadata JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL,
INDEX idx_user_created (user_id, created_at),
INDEX idx_model (model)
) ENGINE=InnoDB;
Design-Entscheidungen erklärt
- AUTO_INCREMENT: IDs werden automatisch vergeben
- NOT NULL: Pflichtfelder (kann nicht leer sein)
- UNIQUE: Wert darf nur einmal vorkommen (z.B. Email)
- DEFAULT: Standardwert wenn nichts angegeben
- ON UPDATE CURRENT_TIMESTAMP: updated_at automatisch aktualisieren
- FOREIGN KEY: Referenz zu anderer Tabelle
- ON DELETE CASCADE: Wenn User gelöscht wird, auch Sessions löschen
- INDEX: Beschleunigt Suchen auf dieser Spalte
Datentypen-Entscheidungen
| Feld | Typ | Warum |
|---|---|---|
| id | INT | Bis 2 Milliarden Einträge |
| VARCHAR(255) | Max Email-Länge nach RFC | |
| session_token | VARCHAR(64) | SHA256-Hash (64 Zeichen) |
| question/answer | TEXT | Bis 65KB (ausreichend für Texte) |
| metadata | JSON | Flexible KI-Metadaten |
| access_level | TINYINT | 0-255 (ausreichend für Levels) |
Indizes: Wann und wo?
Indizes beschleunigen Suchen, verlangsamen aber Inserts. Die Regel:
- INDEX auf Spalten, die Du in WHERE benutzt (z.B. email, session_token)
- INDEX auf Foreign Keys (automatisch bei InnoDB)
- Composite Index für häufige Kombinationen (user_id + created_at)
- KEIN INDEX auf Spalten, die Du nie suchst (z.B. user_agent)
-- Einzelner Index
CREATE INDEX idx_email ON users(email);
-- Composite Index
CREATE INDEX idx_user_created ON chat_logs(user_id, created_at);
-- Unique Index
CREATE UNIQUE INDEX idx_session_token ON sessions(session_token);
-- Index nachträglich hinzufügen
ALTER TABLE users ADD INDEX idx_name (name);
-- Index löschen
ALTER TABLE users DROP INDEX idx_name;
Häufiger Fehler: Zu viele Indizes
Ich sehe oft Tabellen mit 10+ Indizes. Das verlangsamt Inserts massiv. Faustregel: Maximal 3-5 Indizes pro Tabelle, nur auf Spalten die wirklich gesucht werden.
Vollständiges Setup-Script
-- Datenbank erstellen
CREATE DATABASE IF NOT EXISTS ki_system
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE ki_system;
-- Users
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
access_level TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB;
-- Sessions
CREATE TABLE sessions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
session_token VARCHAR(64) NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (session_token)
) ENGINE=InnoDB;
-- Chat-Logs
CREATE TABLE chat_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
question TEXT NOT NULL,
answer TEXT,
model VARCHAR(50),
tokens_generated INT,
metadata JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_user_created (user_id, created_at)
) ENGINE=InnoDB;
-- Verifizieren
SHOW TABLES;
DESCRIBE users;
DESCRIBE sessions;
DESCRIBE chat_logs;
Aus meiner Perspektive
Ich erstelle Schema-Scripts immer als .sql-Dateien im Git-Repo. So kann ich das Schema jederzeit neu aufbauen (Development, Testing, Production). Versionskontrolle für Datenbank-Schemas ist genauso wichtig wie für Code.
CRUD-Operationen: Create, Read, Update, Delete
CRUD sind die vier Grundoperationen für Daten: Create (Erstellen), Read (Lesen), Update (Aktualisieren), Delete (Löschen). Alles andere baut darauf auf.
CRUD sind die vier Grundoperationen für Daten: Create (Erstellen), Read (Lesen), Update (Aktualisieren), Delete (Löschen). Alles andere baut darauf auf.
Create: Daten einfügen
INSERT INTO users (email, name) VALUES ('max@example.com', 'Max');
Fügt einen neuen User ein.
Read: Daten abrufen
SELECT * FROM users WHERE email = 'max@example.com';
Liest den User mit dieser Email.
Update: Daten ändern
UPDATE users SET name = 'Maximilian' WHERE email = 'max@example.com';
Ändert den Namen des Users.
Delete: Daten löschen
DELETE FROM users WHERE email = 'max@example.com';
Löscht den User.
Das sind die Basics. In der Praxis nutzt Du immer Prepared Statements dafür.
CRUD mit Prepared Statements (PHP)
Ich zeige CRUD-Operationen so, wie sie produktiv genutzt werden sollten: Mit Prepared Statements, Fehlerbehandlung und Best Practices.
CREATE: Daten einfügen
// Einfacher Insert
$stmt = $pdo->prepare("
INSERT INTO users (email, name, access_level)
VALUES (?, ?, ?)
");
$stmt->execute([$email, $name, $accessLevel]);
$userId = $pdo->lastInsertId();
// Multiple Inserts (effizient)
$stmt = $pdo->prepare("INSERT INTO users (email, name) VALUES (?, ?)");
$users = [
['max@example.com', 'Max'],
['anna@example.com', 'Anna'],
['tom@example.com', 'Tom']
];
foreach ($users as $user) {
$stmt->execute($user);
}
// Bulk-Insert (noch effizienter)
$stmt = $pdo->prepare("
INSERT INTO users (email, name) VALUES
(?, ?), (?, ?), (?, ?)
");
$stmt->execute([
'max@example.com', 'Max',
'anna@example.com', 'Anna',
'tom@example.com', 'Tom'
]);
READ: Daten abrufen
// Einzelner Datensatz
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch(); // Array
// Mehrere Datensätze
$stmt = $pdo->prepare("SELECT * FROM users WHERE access_level >= ?");
$stmt->execute([2]);
$users = $stmt->fetchAll();
// Nur ein Wert
$stmt = $pdo->prepare("SELECT COUNT(*) as count FROM users");
$stmt->execute();
$count = $stmt->fetchColumn();
// Mit JOIN
$stmt = $pdo->prepare("
SELECT u.name, COUNT(c.id) as chat_count
FROM users u
LEFT JOIN chat_logs c ON u.id = c.user_id
WHERE u.id = ?
GROUP BY u.id
");
$stmt->execute([$userId]);
$result = $stmt->fetch();
UPDATE: Daten aktualisieren
// Einfaches Update
$stmt = $pdo->prepare("UPDATE users SET name = ? WHERE id = ?");
$stmt->execute([$newName, $userId]);
$affectedRows = $stmt->rowCount();
// Mehrere Felder
$stmt = $pdo->prepare("
UPDATE users
SET name = ?, access_level = ?, updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$newName, $newLevel, $userId]);
// Conditional Update
$stmt = $pdo->prepare("
UPDATE users
SET access_level = access_level + 1
WHERE access_level < 5
");
$stmt->execute();
$upgraded = $stmt->rowCount();
DELETE: Daten löschen
// Einzelner Datensatz
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$userId]);
$deleted = $stmt->rowCount();
// Conditional Delete
$stmt = $pdo->prepare("DELETE FROM sessions WHERE expires_at < NOW()");
$stmt->execute();
$cleaned = $stmt->rowCount();
// Mit LIMIT (sicherer für große Deletes)
$stmt = $pdo->prepare("
DELETE FROM chat_logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
LIMIT 1000
");
$stmt->execute();
Vorsicht bei DELETE
NIEMALS:
DELETE FROM users; -- Löscht ALLE User!
IMMER mit WHERE:
DELETE FROM users WHERE id = ?; -- Nur spezifischer User
Ich habe zu viele Horror-Stories gehört von versehentlich gelöschten Production-Datenbanken. WHERE ist Pflicht.
CRUD in Python (pymysql)
import pymysql
conn = pymysql.connect(
host='localhost',
user='ki_user',
password='...',
database='ki_system',
charset='utf8mb4'
)
try:
with conn.cursor() as cursor:
# CREATE
cursor.execute(
"INSERT INTO users (email, name) VALUES (%s, %s)",
('user@example.com', 'User')
)
user_id = cursor.lastrowid
# READ
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
# UPDATE
cursor.execute(
"UPDATE users SET name = %s WHERE id = %s",
('New Name', user_id)
)
# DELETE
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
conn.commit()
except Exception as e:
conn.rollback()
print(f"Error: {e}")
finally:
conn.close()
Aus meiner Erfahrung
CRUD-Operationen sind in 90% der Fälle simpel. Die Komplexität kommt aus:
- Fehlerbehandlung: Was wenn Insert fehlschlägt?
- Transaktionen: Mehrere Operationen atomar
- Performance: Bulk-Operationen statt Loops
- Sicherheit: IMMER Prepared Statements
Wenn Du diese vier Punkte beachtest, bist Du safe. CRUD ist das Fundament, aber der Teufel steckt im Detail.
CRUD für KI-Logs: Praktisches Beispiel
// Ollama-Response loggen
$stmt = $pdo->prepare("
INSERT INTO chat_logs (
user_id, question, answer, model,
tokens_generated, duration_ms, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?)
");
$metadata = json_encode([
'temperature' => 0.7,
'context_length' => 4096,
'stream' => false
]);
$stmt->execute([
$userId,
$question,
$ollamaResponse['response'],
'llama3.2:3b',
$ollamaResponse['eval_count'],
$durationMs,
$metadata
]);
// Log-ID zurückgeben
$logId = $pdo->lastInsertId();
Das ist ein vollständiger Log-Eintrag für ein KI-System. Strukturiert, sicher, nachvollziehbar.
Query-Strategien: SELECT, WHERE, JOIN, Indizes
Queries sind Abfragen, mit denen Du Daten aus der Datenbank holst. Von einfach ("Zeige alle User") bis komplex ("Zeige User mit > 10 Chats im letzten Monat").
Queries sind Abfragen, mit denen Du Daten aus der Datenbank holst. Von einfach ("Zeige alle User") bis komplex ("Zeige User mit > 10 Chats im letzten Monat").
SELECT: Daten abrufen
SELECT * FROM users;
Holt alle Spalten aller User.
WHERE: Filtern
SELECT * FROM users WHERE access_level > 1;
Nur User mit Access-Level größer 1.
JOIN: Tabellen verbinden
SELECT u.name, c.question
FROM users u
JOIN chat_logs c ON u.id = c.user_id;
Zeigt User-Namen mit ihren Fragen.
Diese drei Bausteine decken 90% aller Abfragen ab.
SELECT: Basis und Varianten
Spalten auswählen
-- Alle Spalten
SELECT * FROM users;
-- Spezifische Spalten
SELECT id, email, name FROM users;
-- Mit Alias
SELECT
email AS user_email,
name AS display_name
FROM users;
-- Berechnete Spalten
SELECT
name,
TIMESTAMPDIFF(DAY, created_at, NOW()) as days_since_signup
FROM users;
DISTINCT: Duplikate entfernen
-- Alle verschiedenen Modelle, die genutzt wurden
SELECT DISTINCT model FROM chat_logs;
LIMIT und OFFSET: Pagination
-- Erste 10 Datensätze
SELECT * FROM users LIMIT 10;
-- Datensätze 11-20 (Page 2)
SELECT * FROM users LIMIT 10 OFFSET 10;
-- Alternative Syntax
SELECT * FROM users LIMIT 10, 10; -- Offset 10, Limit 10
WHERE: Komplexe Filter
Vergleichsoperatoren
-- Gleichheit
WHERE access_level = 2
-- Ungleichheit
WHERE access_level != 1
-- Größer/Kleiner
WHERE tokens_generated > 1000
-- BETWEEN
WHERE created_at BETWEEN '2025-01-01' AND '2025-12-31'
-- IN
WHERE model IN ('llama3.2:3b', 'mistral:instruct')
-- LIKE (Pattern-Matching)
WHERE email LIKE '%@gmail.com'
-- IS NULL / IS NOT NULL
WHERE answer IS NOT NULL
Logische Verknüpfungen
-- AND
WHERE access_level > 1 AND created_at > '2025-01-01'
-- OR
WHERE model = 'llama3.2:3b' OR model = 'mistral:instruct'
-- Kombination mit Klammern
WHERE (access_level > 2 OR name LIKE 'Admin%')
AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
JOIN: Tabellen verknüpfen
JOINs kombinieren Daten aus mehreren Tabellen. Es gibt vier Typen:
INNER JOIN (Standard)
-- Nur Datensätze, wo Verknüpfung existiert
SELECT
u.name,
c.question,
c.created_at
FROM users u
INNER JOIN chat_logs c ON u.id = c.user_id
ORDER BY c.created_at DESC;
LEFT JOIN
-- Alle User, auch ohne Chat-Logs
SELECT
u.name,
COUNT(c.id) as chat_count
FROM users u
LEFT JOIN chat_logs c ON u.id = c.user_id
GROUP BY u.id;
Mehrere JOINs
SELECT
u.name,
s.session_token,
c.question,
c.answer
FROM users u
LEFT JOIN sessions s ON u.id = s.user_id AND s.expires_at > NOW()
LEFT JOIN chat_logs c ON u.id = c.user_id
WHERE u.access_level > 1
ORDER BY c.created_at DESC
LIMIT 100;
Performance-Falle: N+1 Problem
Häufiger Fehler in Code:
// SCHLECHT: N+1 Queries
$users = $pdo->query("SELECT * FROM users")->fetchAll();
foreach ($users as $user) {
// Für jeden User eine Query!
$stmt = $pdo->prepare("SELECT COUNT(*) FROM chat_logs WHERE user_id = ?");
$stmt->execute([$user['id']]);
$count = $stmt->fetchColumn();
}
// GUT: Eine Query mit JOIN
$result = $pdo->query("
SELECT u.*, COUNT(c.id) as chat_count
FROM users u
LEFT JOIN chat_logs c ON u.id = c.user_id
GROUP BY u.id
")->fetchAll();
Das erste Beispiel macht 1 + N Queries (1 für User, N für Counts). Das zweite macht 1 Query total. Bei 1000 Usern: 1001 Queries vs. 1 Query. Faktor 1000 Unterschied!
Aggregationen: COUNT, SUM, AVG, MAX, MIN
-- Anzahl Chat-Logs pro User
SELECT
user_id,
COUNT(*) as total_chats,
SUM(tokens_generated) as total_tokens,
AVG(duration_ms) as avg_duration,
MAX(tokens_generated) as max_tokens
FROM chat_logs
GROUP BY user_id
HAVING total_chats > 10;
GROUP BY vs. HAVING
- WHERE: Filtert Zeilen VOR der Gruppierung
- HAVING: Filtert Gruppen NACH der Aggregation
-- User mit > 100 Tokens, gruppiert, nur Gruppen mit > 5 Chats
SELECT user_id, COUNT(*) as chats, SUM(tokens_generated) as tokens
FROM chat_logs
WHERE tokens_generated > 100 -- Filter VOR Gruppierung
GROUP BY user_id
HAVING chats > 5; -- Filter NACH Gruppierung
Indizes: Performance drastisch verbessern
Ohne Index macht MariaDB einen Full-Table-Scan (alle Zeilen durchgehen). Mit Index findet es Daten in Millisekunden.
Benchmark-Beispiel (1 Million Zeilen):
-- Ohne Index
SELECT * FROM chat_logs WHERE user_id = 123;
-- Dauer: ~800ms (Full Scan)
-- Mit Index
CREATE INDEX idx_user_id ON chat_logs(user_id);
SELECT * FROM chat_logs WHERE user_id = 123;
-- Dauer: ~2ms (Index Lookup)
-- 400x schneller!
Composite Index für kombinierte Abfragen
-- Häufige Query
SELECT * FROM chat_logs
WHERE user_id = ? AND created_at > ?;
-- Composite Index (Reihenfolge wichtig!)
CREATE INDEX idx_user_created ON chat_logs(user_id, created_at);
-- Index wird genutzt wenn WHERE beide Spalten filtert
-- oder nur die erste (user_id)
-- NICHT wenn nur created_at gefiltert wird
EXPLAIN: Queries analysieren
EXPLAIN SELECT * FROM chat_logs WHERE user_id = 123;
-- Ausgabe zeigt:
-- - Welche Indizes genutzt werden
-- - Wie viele Zeilen gescannt werden
-- - Query-Typ (index, range, ALL)
-- ALL = Full Scan (schlecht!)
-- index/range = Index wird genutzt (gut!)
Aus meiner Perspektive
Ich nutze EXPLAIN für jede Query, die langsam ist. Es zeigt sofort, ob Indizes fehlen. Die Regel: Wenn EXPLAIN "ALL" zeigt und die Tabelle > 1000 Zeilen hat, brauchst Du einen Index.
Für KI-Logs mit Millionen Einträgen sind Indizes unverzichtbar. Ohne sind Auswertungen ("Zeige Performance pro Modell") unbenutzbar langsam. Mit Indizes instant.
Best Practices für Queries
- SELECT nur benötigte Spalten, nicht *
- WHERE immer mit Index-Spalten
- LIMIT bei großen Ergebnissen
- JOIN statt Subqueries (meist schneller)
- EXPLAIN nutzen bei Performance-Problemen
- Prepared Statements IMMER
Normalisierung: Daten sinnvoll strukturieren
Normalisierung bedeutet: Redundanz vermeiden. Statt dieselben Daten mehrfach zu speichern, legst Du sie einmal ab und verknüpfst sie.
Normalisierung bedeutet: Redundanz vermeiden. Statt dieselben Daten mehrfach zu speichern, legst Du sie einmal ab und verknüpfst sie.
Problem: Redundanz
Stell Dir vor, Du speicherst bei jeder Bestellung den kompletten User-Namen und die Adresse. Wenn der User umzieht, musst Du hunderte Bestellungen aktualisieren.
Lösung: Normalisierung
Du speicherst User-Daten einmal in der users-Tabelle. Bei Bestellungen speicherst Du nur die User-ID. Wenn der User umzieht, änderst Du einen Datensatz.
Das Prinzip: Jede Information einmal speichern, überall referenzieren.
Die Normalformen
Normalisierung hat verschiedene Stufen (Normalformen). Für die meisten Anwendungen reichen die ersten drei.
Unnormalisiert: Das Chaos
-- SCHLECHT: Alles in einer Tabelle
CREATE TABLE orders_bad (
id INT PRIMARY KEY,
user_email VARCHAR(255),
user_name VARCHAR(100),
user_address TEXT,
product_name VARCHAR(200),
product_price DECIMAL(10,2),
order_date TIMESTAMP
);
-- Problem 1: User-Daten redundant (bei jeder Bestellung wiederholt)
-- Problem 2: Adress-Änderung betrifft alle Bestellungen
-- Problem 3: Wenn Produkt-Preis ändert, stimmen alte Bestellungen nicht
1. Normalform (1NF): Atomare Werte
Jede Spalte enthält nur atomare (unteilbare) Werte. Keine Listen, keine verschachtelten Strukturen.
-- SCHLECHT
CREATE TABLE users_bad (
id INT PRIMARY KEY,
emails TEXT -- 'max@example.com, max@gmail.com' als String
);
-- GUT
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(255)
);
CREATE TABLE user_emails (
user_id INT,
email VARCHAR(255),
PRIMARY KEY (user_id, email),
FOREIGN KEY (user_id) REFERENCES users(id)
);
2. Normalform (2NF): Keine Teil-Abhängigkeiten
Jede Nicht-Schlüssel-Spalte hängt vom gesamten Primärschlüssel ab, nicht nur von Teilen.
-- SCHLECHT: product_name hängt nur von product_id ab, nicht von order_id
CREATE TABLE order_items_bad (
order_id INT,
product_id INT,
product_name VARCHAR(200), -- Redundant!
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- GUT: Produkt-Daten in eigener Tabelle
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(200),
price DECIMAL(10,2)
);
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
3. Normalform (3NF): Keine transitiven Abhängigkeiten
Nicht-Schlüssel-Spalten hängen direkt vom Primärschlüssel ab, nicht von anderen Nicht-Schlüssel-Spalten.
-- SCHLECHT: city_zipcode hängt von city ab, nicht von user_id
CREATE TABLE users_bad (
id INT PRIMARY KEY,
name VARCHAR(100),
city VARCHAR(100),
city_zipcode VARCHAR(10) -- Abhängig von city, nicht von id!
);
-- GUT: Adressen in eigener Tabelle
CREATE TABLE cities (
id INT PRIMARY KEY,
name VARCHAR(100),
zipcode VARCHAR(10)
);
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
city_id INT,
FOREIGN KEY (city_id) REFERENCES cities(id)
);
Normalisierung im KI-Stack: Praktisches Beispiel
Szenario: Chat-System mit Models
Schlecht normalisiert:
CREATE TABLE chat_logs_bad (
id INT PRIMARY KEY,
user_email VARCHAR(255),
user_name VARCHAR(100),
model_name VARCHAR(50),
model_size VARCHAR(20),
model_provider VARCHAR(50),
question TEXT,
answer TEXT
);
-- Problem: Bei jedem Chat werden User- UND Model-Daten wiederholt
Gut normalisiert (3NF):
-- Users (einmal pro User)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE,
name VARCHAR(100)
);
-- Models (einmal pro Modell)
CREATE TABLE models (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) UNIQUE,
size VARCHAR(20),
provider VARCHAR(50)
);
-- Chat-Logs (nur Referenzen!)
CREATE TABLE chat_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
model_id INT NOT NULL,
question TEXT NOT NULL,
answer TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (model_id) REFERENCES models(id)
);
-- Vorteil: User-/Model-Daten einmal ändern, überall aktuell
Wann NICHT normalisieren (Denormalisierung)
Manchmal ist bewusste Redundanz sinnvoll:
- Performance-kritische Queries: JOINs vermeiden
- Read-Heavy-Workloads: Vorberechnete Werte speichern
- Historische Daten: Snapshot-Prinzip (Preis zum Bestellzeitpunkt)
-- Denormalisiert für Performance
CREATE TABLE chat_logs_denorm (
id INT PRIMARY KEY,
user_id INT,
user_name VARCHAR(100), -- Redundant, aber für schnelle Anzeige
model VARCHAR(50),
question TEXT,
answer TEXT
);
-- Trade-off: Schnellere SELECTs, langsamere UPDATEs
Aus meiner Erfahrung
Ich normalisiere bis 3NF für kritische Daten (User, Permissions, Transaktionen). Für Logs denormalisiere ich bewusst, weil sie Read-Only sind und Performance wichtiger ist als Redundanz-Vermeidung.
Die Regel: Normalisiere für Daten die sich ändern, denormalisiere für Daten die nur gelesen werden. KI-Logs ändern sich nie, also können sie denormalisiert sein.
Foreign Keys und Relationen: Datenintegrität gewährleisten
Foreign Keys sind Verweise zwischen Tabellen. Sie stellen sicher, dass Daten konsistent bleiben.
Foreign Keys sind Verweise zwischen Tabellen. Sie stellen sicher, dass Daten konsistent bleiben.
Das Problem ohne Foreign Keys
Wenn Du einen User löschst, bleiben seine Chat-Logs in der Datenbank. Die verweisen dann auf eine User-ID, die nicht mehr existiert. Das sind verwaiste Daten.
Die Lösung: Foreign Keys
Du definierst: "Die Spalte user_id in chat_logs verweist auf id in users". MariaDB verhindert dann automatisch:
- User löschen, wenn noch Chats existieren (oder löscht Chats mit)
- Chat-Log mit nicht-existierender User-ID anlegen
Foreign Keys erzwingen Datenintegrität durch Design. Die Datenbank übernimmt die Konsistenz-Checks.
Foreign Keys im Detail
Ein Foreign Key definiert eine Referenz von einer Tabelle zu einer anderen. Die referenzierte Spalte muss PRIMARY KEY oder UNIQUE sein.
Syntax und Optionen
CREATE TABLE chat_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
question TEXT,
-- Foreign Key mit Optionen
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
Wichtige Elemente:
FOREIGN KEY (user_id): Diese Spalte ist der Foreign KeyREFERENCES users(id): Verweist auf users.idON DELETE CASCADE: Was passiert wenn User gelöscht wirdON UPDATE CASCADE: Was passiert wenn users.id ändert (selten)
ON DELETE Optionen
| Option | Verhalten | Use-Case |
|---|---|---|
CASCADE |
Abhängige Zeilen auch löschen | Sessions wenn User weg |
SET NULL |
Foreign Key auf NULL setzen | Logs behalten, User-Referenz entfernen |
RESTRICT |
Löschen verhindern | User nur löschen wenn keine Chats |
NO ACTION |
Wie RESTRICT | Standard-Verhalten |
Praktische Beispiele
CASCADE: Sessions mit User löschen
CREATE TABLE sessions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
session_token VARCHAR(64),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- User löschen
DELETE FROM users WHERE id = 123;
-- Resultat: User UND alle seine Sessions sind weg (automatisch!)
SET NULL: Logs behalten, Referenz entfernen
CREATE TABLE chat_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT, -- NULL erlaubt!
question TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
-- User löschen
DELETE FROM users WHERE id = 123;
-- Resultat: Logs bleiben, user_id ist jetzt NULL
RESTRICT: Löschen verhindern
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
amount DECIMAL(10,2),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT
);
-- User löschen versuchen
DELETE FROM users WHERE id = 123;
-- Fehler: Cannot delete or update a parent row: a foreign key constraint fails
-- User kann nur gelöscht werden wenn KEINE Orders existieren
Mehrere Foreign Keys
CREATE TABLE chat_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
session_id INT,
model_id INT NOT NULL,
question TEXT,
answer TEXT,
-- Mehrere Foreign Keys
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL,
FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE RESTRICT
);
-- user_id: User weg = Logs weg (CASCADE)
-- session_id: Session weg = Logs behalten, Referenz NULL (SET NULL)
-- model_id: Model kann nicht gelöscht werden wenn Logs existieren (RESTRICT)
Performance-Impact von Foreign Keys
Foreign Keys haben einen kleinen Performance-Overhead:
- INSERT: Prüft ob referenzierte ID existiert (circa 1-5ms Overhead)
- DELETE: Prüft CASCADE/SET NULL Aktionen (kann langsam sein bei vielen Abhängigkeiten)
- UPDATE: Selten relevant (IDs ändern sich nicht)
Für die meisten Anwendungen ist das irrelevant. Die Datenintegrität ist wichtiger als 5ms.
Foreign Keys nachträglich hinzufügen/entfernen
-- Foreign Key hinzufügen
ALTER TABLE chat_logs
ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- Foreign Key entfernen (Name via SHOW CREATE TABLE finden)
ALTER TABLE chat_logs
DROP FOREIGN KEY chat_logs_ibfk_1;
-- Alle Constraints anzeigen
SELECT
CONSTRAINT_NAME,
TABLE_NAME,
REFERENCED_TABLE_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = 'ki_system' AND REFERENCED_TABLE_NAME IS NOT NULL;
Aus meiner Perspektive
Ich nutze Foreign Keys zu 100% in Production-Systemen. Sie haben mich schon oft vor Dateninkonsistenzen gerettet. Besonders in KI-Systemen, wo Daten aus verschiedenen Quellen kommen (User-Input, Ollama-Output, ChromaDB-Metadaten), sind Foreign Keys unverzichtbar.
Das Tückische ohne Foreign Keys: Alles funktioniert, bis Du merkst, dass Du verwaiste Datensätze hast und nicht mehr weißt, welcher User welchen Chat hatte. Dann ist es zu spät.
Best Practices
- IMMER Foreign Keys für Referenzen (außer bei extremen Performance-Anforderungen)
- CASCADE nutzen für "Besitz"-Relationen (User besitzt Sessions)
- SET NULL nutzen für optionale Referenzen (Logs mit optionaler Session)
- RESTRICT nutzen für kritische Daten (User mit Bestellungen nicht löschbar)
- InnoDB Engine verwenden (MyISAM unterstützt keine Foreign Keys)
Wann SQL, wann Vektordatenbank: Die richtige Wahl treffen
Die Entscheidung ist eigentlich einfach: Strukturierte Daten in MariaDB, semantische Suche in ChromaDB.
Die Entscheidung ist eigentlich einfach: Strukturierte Daten in MariaDB, semantische Suche in ChromaDB.
Nutze MariaDB wenn:
- Du User-Accounts, Permissions, Transaktionen speicherst
- Daten Beziehungen haben (User hat Bestellungen)
- Du exakte Suchen brauchst (email = 'max@example.com')
- ACID-Garantien wichtig sind (Zahlungen, Bestellungen)
- Du Aggregationen brauchst (SUM, COUNT, AVG)
Nutze ChromaDB wenn:
- Du Texte semantisch durchsuchen willst
- Du "Finde ähnliche Dokumente" brauchst
- Du RAG (Retrieval-Augmented Generation) baust
- Content-Suche wichtiger ist als Struktur
- Du mit Embeddings arbeitest
Oft brauchst Du beides
Ein Chat-System speichert User-Daten in MariaDB, Chat-Inhalte in ChromaDB. Beide sind über IDs verknüpft. Das Beste aus beiden Welten.
Entscheidungsmatrix
Die Wahl zwischen SQL und Vektordatenbank hängt von mehreren Faktoren ab. Ich zeige das an konkreten Use-Cases.
Datentyp-basierte Entscheidung
| Datentyp | Datenbank | Begründung |
|---|---|---|
| User-Accounts | MariaDB | Strukturiert, Relationen, ACID |
| Transaktionen | MariaDB | Atomarität erforderlich |
| Sessions | MariaDB | Foreign Key zu User |
| Performance-Logs | MariaDB | Zeitreihen, Aggregationen |
| Dokumentation | ChromaDB | Semantische Suche |
| Knowledge-Base | ChromaDB | RAG, Ähnlichkeitssuche |
| Chat-Historie | Beide | Metadaten in MariaDB, Content in ChromaDB |
Hybrid-Ansatz: Das Beste aus beiden Welten
In der Praxis kombinierst Du beide Datenbanken. Ich zeige das an einem konkreten RAG-System.
Szenario: Dokumentations-Chatbot
MariaDB speichert:
-- User und Permissions
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(255),
access_level INT
);
-- Chat-Sessions (Metadaten)
CREATE TABLE chat_sessions (
id INT PRIMARY KEY,
user_id INT,
model VARCHAR(50),
created_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Performance-Metriken
CREATE TABLE chat_metrics (
id INT PRIMARY KEY,
session_id INT,
tokens_generated INT,
duration_ms INT,
FOREIGN KEY (session_id) REFERENCES chat_sessions(id)
);
ChromaDB speichert:
# Dokumentation als Embeddings
collection = chroma_client.create_collection("docs")
collection.add(
documents=["Ollama Installation: curl -fsSL ..."],
metadatas=[{
"doc_id": "doc_123",
"category": "installation",
"access_level": 1
}],
ids=["doc_123"]
)
Zusammenspiel im Code:
# Python
import pymysql
import chromadb
import ollama
# 1. User aus MariaDB laden
conn = pymysql.connect(...)
cursor = conn.cursor()
cursor.execute("SELECT id, access_level FROM users WHERE email = %s", (email,))
user = cursor.fetchone()
# 2. Relevante Docs aus ChromaDB (mit Access-Check!)
collection = chroma_client.get_collection("docs")
results = collection.query(
query_texts=[question],
n_results=3,
where={"access_level": {"$lte": user['access_level']}}
)
# 3. Ollama-Antwort generieren
context = "\n\n".join(results['documents'][0])
response = ollama.generate(
model='llama3.2:3b',
prompt=f"Context: {context}\n\nFrage: {question}"
)
# 4. Session + Metriken in MariaDB loggen
cursor.execute(
"INSERT INTO chat_sessions (user_id, model) VALUES (%s, %s)",
(user['id'], 'llama3.2:3b')
)
session_id = cursor.lastrowid
cursor.execute(
"INSERT INTO chat_metrics (session_id, tokens_generated, duration_ms) VALUES (%s, %s, %s)",
(session_id, response['eval_count'], duration_ms)
)
conn.commit()
conn.close()
Warum nicht alles in MariaDB?
Du könntest Dokumentation in MariaDB als TEXT-Spalten speichern:
CREATE TABLE docs (
id INT PRIMARY KEY,
title VARCHAR(200),
content TEXT
);
-- Suche
SELECT * FROM docs WHERE content LIKE '%Installation%';
Problem: LIKE ist langsam und findet nur exakte Wort-Matches. "Wie installiere ich?" findet nicht "Installation und Setup". Keine Semantik.
Warum nicht alles in ChromaDB?
Du könntest User-Daten in ChromaDB speichern:
collection.add(
documents=["User: max@example.com"],
metadatas={"email": "max@example.com", "name": "Max"},
ids=["user_123"]
)
Problem: Keine Foreign Keys, keine Transaktionen, keine Relationen. User-Bestellungen zu verknüpfen wird kompliziert.
Decision Tree
Frage: Welche Datenbank?
│
├─ Strukturierte Daten mit Relationen? → MariaDB
│ (Users, Orders, Sessions, Permissions)
│
├─ Semantische Textsuche? → ChromaDB
│ (Documents, FAQs, Knowledge-Base)
│
├─ Zeitreihen-Analysen? → MariaDB
│ (Logs, Metriken, Analytics)
│
├─ Embedding-basierte Suche? → ChromaDB
│ (Ähnliche Dokumente, RAG-Context)
│
└─ ACID-Garantien erforderlich? → MariaDB
(Zahlungen, Bestellungen, kritische Daten)
Aus meiner Erfahrung
Die Frage ist nicht "MariaDB ODER ChromaDB", sondern "MariaDB UND ChromaDB". Im KI-Stack brauchst Du beide:
- MariaDB: Alles was Struktur, Relationen oder ACID braucht
- ChromaDB: Alles was semantische Suche braucht
- Ollama: LLM-Inferenz für beide
Ich habe Systeme gesehen, die versuchen, mit einer Datenbank alles zu machen. Das funktioniert nie optimal. Nutze jedes Tool für seine Stärke.
Migrations-Strategien
Manchmal merkst Du erst später, dass Du die falsche Datenbank gewählt hast. Migrations-Szenarien:
Von MariaDB zu ChromaDB (Text-Spalten)
# Python-Script
import pymysql
import chromadb
# Daten aus MariaDB laden
conn = pymysql.connect(...)
cursor = conn.cursor()
cursor.execute("SELECT id, title, content FROM docs")
docs = cursor.fetchall()
# Nach ChromaDB migrieren
collection = chroma_client.create_collection("docs")
for doc in docs:
collection.add(
documents=[doc['content']],
metadatas=[{"title": doc['title'], "original_id": doc['id']}],
ids=[f"doc_{doc['id']}"]
)
# MariaDB-Tabelle behalten für Metadaten,
# content-Spalte kann geleert werden
Von ChromaDB zu MariaDB (Metadaten)
# Falls Du merkst, dass Du doch Relationen brauchst
collection = chroma_client.get_collection("users")
results = collection.get(include=['metadatas'])
# Nach MariaDB migrieren
for i, metadata in enumerate(results['metadatas']):
cursor.execute(
"INSERT INTO users (email, name, access_level) VALUES (%s, %s, %s)",
(metadata['email'], metadata['name'], metadata.get('access_level', 1))
)
Hybrid ist OK, Chaos nicht
Es ist OK, denselben Datentyp in beiden Datenbanken zu haben (z.B. Chat-Metadaten in MariaDB, Chat-Content in ChromaDB). Das ist bewusste Redundanz für Performance.
Es ist NICHT OK, dieselben Daten inkonsistent zu halten (User-Email in MariaDB = max@old.com, in ChromaDB = max@new.com). Dann hast Du ein Datenproblem.
Aus meiner Perspektive
Meine Faustregel nach Jahren mit KI-Systemen:
- Alles mit ID: MariaDB (users.id, products.id)
- Alles mit Text: ChromaDB (docs, faqs)
- Metadaten: Beide (MariaDB für Struktur, ChromaDB für Filterung)
- Single Source of Truth: MariaDB (weil ACID)
- Search-Index: ChromaDB (weil semantisch)
Wenn Du unsicher bist: Starte mit MariaDB. Du kannst später immer noch ChromaDB für Suche ergänzen. Andersherum (ChromaDB → MariaDB) ist schmerzhafter.
Backup, Recovery und Migration
Backups sind Pflicht. Daten können durch Fehler, Hardware-Ausfälle oder Angriffe verloren gehen. Ein Backup rettet Dich.
Backups sind Pflicht. Daten können durch Fehler, Hardware-Ausfälle oder Angriffe verloren gehen. Ein Backup rettet Dich.
Backup erstellen
mysqldump -u root -p ki_system > backup.sql
Das erstellt eine Datei mit allen Daten und Tabellen-Strukturen. Du kannst sie jederzeit wiederherstellen.
Backup wiederherstellen
mysql -u root -p ki_system < backup.sql
Lädt alle Daten zurück in die Datenbank.
Mach Backups regelmäßig. Täglich für Production-Systeme, vor jeder größeren Änderung.
Backup-Strategien
Es gibt verschiedene Backup-Typen. Die Wahl hängt von Größe, Änderungshäufigkeit und Recovery-Zeit-Anforderungen ab.
1. Logisches Backup (mysqldump)
Exportiert Daten als SQL-Statements. Langsam bei großen Datenbanken, aber portabel.
Komplette Datenbank
# Alle Daten + Strukturen
mysqldump -u root -p ki_system > backup_full.sql
# Mit Kompression
mysqldump -u root -p ki_system | gzip > backup_full.sql.gz
# Mehrere Datenbanken
mysqldump -u root -p --databases ki_system analytics > backup_multi.sql
# Alle Datenbanken
mysqldump -u root -p --all-databases > backup_all.sql
Nur Strukturen (ohne Daten)
mysqldump -u root -p --no-data ki_system > schema.sql
Nur Daten (ohne Strukturen)
mysqldump -u root -p --no-create-info ki_system > data.sql
Einzelne Tabelle
mysqldump -u root -p ki_system chat_logs > chat_logs_backup.sql
Performance-Hinweis
mysqldump lockt Tabellen während des Exports (kurz). Bei großen Datenbanken (> 10GB) kann das Minuten dauern. Nutze --single-transaction für InnoDB-Tabellen (kein Lock):
mysqldump -u root -p --single-transaction ki_system > backup.sql
2. Physisches Backup (File-Copy)
Kopiert die Datenbank-Dateien direkt. Schnell, aber weniger portabel.
# MariaDB stoppen
sudo systemctl stop mariadb
# Datenverzeichnis kopieren
sudo cp -r /var/lib/mysql/ki_system /backup/mysql/ki_system_$(date +%Y%m%d)
# MariaDB starten
sudo systemctl start mariadb
# Wiederherstellung
sudo systemctl stop mariadb
sudo rm -rf /var/lib/mysql/ki_system
sudo cp -r /backup/mysql/ki_system_20251111 /var/lib/mysql/ki_system
sudo chown -R mysql:mysql /var/lib/mysql/ki_system
sudo systemctl start mariadb
3. Inkrementelles Backup (Binary Logs)
Speichert nur Änderungen seit letztem Full-Backup. Optimal für große Datenbanken.
# Binary Logging aktivieren (/etc/mysql/mariadb.conf.d/50-server.cnf)
[mysqld]
log_bin = /var/log/mysql/mariadb-bin
expire_logs_days = 7
max_binlog_size = 100M
# Neustart
sudo systemctl restart mariadb
# Binlogs anzeigen
SHOW BINARY LOGS;
# Backup-Workflow:
# 1. Full Backup (Sonntag)
mysqldump --single-transaction --flush-logs ki_system > full_backup.sql
# 2. Inkrementelle Backups (täglich via Binary Logs)
# Logs werden automatisch erstellt
# 3. Recovery (Point-in-Time)
# Full Backup einspielen
mysql ki_system < full_backup.sql
# Binary Logs bis zu gewünschtem Zeitpunkt anwenden
mysqlbinlog mariadb-bin.000001 mariadb-bin.000002 | mysql ki_system
Recovery-Szenarien
Szenario 1: Tabelle versehentlich gelöscht
# Problem
DROP TABLE chat_logs; -- Ups!
# Lösung
mysql ki_system < backup.sql
# Oder nur die Tabelle
mysql ki_system < backup.sql --one-database chat_logs
Szenario 2: Fehlerhafte UPDATE-Query
# Problem
UPDATE users SET email = 'wrong@example.com'; -- Vergessen WHERE!
# Alle Emails überschrieben!
# Lösung 1: Aus Backup wiederherstellen
mysql ki_system < backup.sql
# Lösung 2: Point-in-Time Recovery (wenn Binary Logs aktiv)
# Backup bis kurz vor dem Fehler einspielen
mysqlbinlog --stop-datetime="2025-11-11 14:30:00" mariadb-bin.000123 | mysql ki_system
Automated Backups
# Cron-Job für tägliche Backups
# /etc/cron.daily/mariadb-backup.sh
#!/bin/bash
BACKUP_DIR="/backup/mariadb"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
mysqldump -u backup_user -p'password' \
--single-transaction \
--routines \
--triggers \
ki_system | gzip > $BACKUP_DIR/ki_system_$DATE.sql.gz
# Alte Backups löschen (> 30 Tage)
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete
# Ausführbar machen
chmod +x /etc/cron.daily/mariadb-backup.sh
Aus meiner Erfahrung
Ich mache Full-Backups täglich (nachts um 3 Uhr), Binary Logs kontinuierlich. Recovery-Zeit: Circa 15 Minuten für 10GB Datenbank.
Das Wichtigste: Backups testen. Ich restore monatlich auf einem Test-Server. Zu viele Unternehmen haben Backups, die nicht funktionieren. Das merkst Du erst im Notfall.
Cloud-Backup-Optionen
# S3-Upload (nach Backup)
aws s3 cp backup.sql.gz s3://my-backups/mariadb/$(date +%Y%m%d)/
# Oder rclone für andere Cloud-Provider
rclone copy backup.sql.gz remote:backups/mariadb/
Backup-Verschlüsselung
Backups enthalten sensitive Daten. Verschlüssele sie:
# Mit GPG
mysqldump ki_system | gzip | gpg --encrypt --recipient admin@example.com > backup.sql.gz.gpg
# Decrypt
gpg --decrypt backup.sql.gz.gpg | gunzip | mysql ki_system
Performance-Optimierung: Query-Tuning und Caching
Langsame Queries sind das häufigste Performance-Problem. Die Lösung ist meist simpel: Indizes hinzufügen.
Langsame Queries sind das häufigste Performance-Problem. Die Lösung ist meist simpel: Indizes hinzufügen.
Problem: Langsame Query
SELECT * FROM chat_logs WHERE user_id = 123;
Dauert 800ms bei 1 Million Zeilen.
Lösung: Index
CREATE INDEX idx_user_id ON chat_logs(user_id);
Jetzt dauert dieselbe Query 2ms. Problem gelöst.
Das deckt 80% aller Performance-Probleme ab. Fehlende Indizes sind der häufigste Grund für langsame Datenbanken.
Systematisches Query-Tuning
Performance-Optimierung folgt einem klaren Prozess. Ich zeige den Workflow, den ich seit Jahren nutze.
Schritt 1: Langsame Queries identifizieren
-- Slow Query Log aktivieren
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1; -- Queries > 1 Sekunde
-- Log-Datei ansehen
sudo tail -f /var/log/mysql/slow.log
-- Top 10 langsamste Queries analysieren
sudo mysqldumpslow -t 10 /var/log/mysql/slow.log
Schritt 2: EXPLAIN nutzen
EXPLAIN SELECT * FROM chat_logs WHERE user_id = 123;
-- Wichtige Spalten:
-- type: ALL (schlecht), index/range (gut)
-- possible_keys: Welche Indizes verfügbar
-- key: Welcher Index tatsächlich genutzt
-- rows: Wie viele Zeilen gescannt werden
Beispiel-Ausgabe:
+----+-------------+------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | chat_logs | ALL | NULL | NULL | NULL | NULL | 980123 | Using where |
+----+-------------+------------+------+---------------+------+---------+------+--------+-------------+
-- type=ALL, rows=980123 → Full Table Scan! Index fehlt!
Schritt 3: Index hinzufügen
CREATE INDEX idx_user_id ON chat_logs(user_id);
-- Erneut EXPLAIN
EXPLAIN SELECT * FROM chat_logs WHERE user_id = 123;
-- Jetzt:
-- type=ref, key=idx_user_id, rows=234 → Index wird genutzt!
Häufige Performance-Probleme und Lösungen
Problem 1: SELECT *
-- LANGSAM (transferiert viele Daten)
SELECT * FROM chat_logs WHERE user_id = 123;
-- SCHNELL (nur benötigte Spalten)
SELECT id, question, created_at FROM chat_logs WHERE user_id = 123;
Problem 2: N+1 Queries
-- LANGSAM (1 + N Queries)
$users = $pdo->query("SELECT * FROM users")->fetchAll();
foreach ($users as $user) {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM chat_logs WHERE user_id = ?");
$stmt->execute([$user['id']]);
}
-- SCHNELL (1 Query)
SELECT u.id, u.name, COUNT(c.id) as chat_count
FROM users u
LEFT JOIN chat_logs c ON u.id = c.user_id
GROUP BY u.id;
Problem 3: Fehlende Indizes bei JOINs
-- JOIN ist langsam wenn Foreign Key keinen Index hat
SELECT u.name, c.question
FROM users u
JOIN chat_logs c ON u.id = c.user_id;
-- Lösung: Index auf Foreign Key
CREATE INDEX idx_user_id ON chat_logs(user_id);
-- InnoDB erstellt Foreign Key Indizes automatisch,
-- aber prüfe mit:
SHOW INDEX FROM chat_logs;
Problem 4: OR in WHERE
-- LANGSAM (Index kann nicht genutzt werden)
SELECT * FROM chat_logs WHERE user_id = 123 OR user_id = 456;
-- SCHNELL (Index wird genutzt)
SELECT * FROM chat_logs WHERE user_id IN (123, 456);
Query-Cache (deprecated, aber erwähnenswert)
In MariaDB < 10.x gab es einen Query-Cache. Ab 10.x ist er entfernt. Die Empfehlung: Application-Level-Caching.
# PHP: Redis für Query-Caching
$cacheKey = "user_stats_" . $userId;
$cached = $redis->get($cacheKey);
if ($cached) {
return json_decode($cached, true);
}
// Query ausführen
$stmt = $pdo->prepare("SELECT COUNT(*) as count FROM chat_logs WHERE user_id = ?");
$stmt->execute([$userId]);
$result = $stmt->fetch();
// 5 Minuten cachen
$redis->setex($cacheKey, 300, json_encode($result));
return $result;
Connection-Pooling
Für KI-Systeme mit vielen Requests ist Connection-Pooling wichtig:
# PHP: Persistent Connections
$pdo = new PDO(
'mysql:host=localhost;dbname=ki_system',
'user',
'password',
[PDO::ATTR_PERSISTENT => true] // Connection wird wiederverwendet
);
# Python: SQLAlchemy mit Pool
from sqlalchemy import create_engine
engine = create_engine(
'mysql+pymysql://user:pass@localhost/ki_system',
pool_size=10, # 10 Connections im Pool
max_overflow=20, # Bis zu 30 total
pool_pre_ping=True # Connection-Check vor Nutzung
)
Monitoring ist Pflicht
Du musst wissen, welche Queries langsam sind. Tools:
- Slow Query Log: Built-in, einfach
- Performance Schema: Detaillierte Metriken
- pt-query-digest: Percona Tool für Log-Analyse
- Grafana + Prometheus: Echtzeit-Monitoring
Performance-Checkliste
- Indizes auf WHERE-Spalten ✓
- Indizes auf Foreign Keys ✓
- SELECT nur benötigte Spalten ✓
- LIMIT bei großen Results ✓
- JOINs statt Subqueries ✓
- Batch-Inserts statt Loops ✓
- Connection-Pooling aktiviert ✓
- Slow Query Log aktiv ✓
Aus meiner Erfahrung
Performance-Probleme sind zu 90% fehlende Indizes. Ich starte immer mit EXPLAIN. Wenn type=ALL bei > 1000 Zeilen, fehlt ein Index.
Für KI-Logs mit Millionen Einträgen sind Composite-Indizes Gold wert. INDEX (user_id, created_at) beschleunigt "Zeige letzte Chats von User X" massiv.
Produktive Systeme: Monitoring und Skalierung
Von der Entwicklung in die Production: Es gibt einige wichtige Unterschiede zwischen einem Test-System und einem produktiven System.
Von der Entwicklung in die Production: Es gibt einige wichtige Unterschiede zwischen einem Test-System und einem produktiven System.
Monitoring: Wissen was läuft
In Production musst Du sehen können:
- Wie viele Queries laufen pro Sekunde?
- Welche Queries sind langsam?
- Wie viel Speicher nutzt die Datenbank?
- Gibt es Fehler oder Warnungen?
Backups: Automatisch und getestet
Tägliche Backups sind Pflicht. Teste sie regelmäßig durch Restore auf einem Test-Server.
Skalierung: Wenn ein Server nicht reicht
Bei sehr hoher Last kannst Du MariaDB replizieren: Ein Master für Writes, mehrere Slaves für Reads. Das verteilt die Last.
Production-Readiness Checkliste
Bevor ein MariaDB-System in Production geht, prüfe ich diese Punkte systematisch.
1. Monitoring aufsetzen
System-Metriken (Performance Schema)
-- Performance Schema aktivieren
-- /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
performance_schema = ON
-- Wichtige Queries:
-- Aktive Connections
SELECT COUNT(*) as active_connections
FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep';
-- Langsame Queries (Live)
SELECT
DIGEST_TEXT,
COUNT_STAR as executions,
AVG_TIMER_WAIT / 1000000000 as avg_seconds
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000 -- > 1 Sekunde
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
-- Table-Größen
SELECT
table_name,
ROUND(data_length / 1024 / 1024, 2) as data_mb,
ROUND(index_length / 1024 / 1024, 2) as index_mb
FROM information_schema.TABLES
WHERE table_schema = 'ki_system'
ORDER BY data_length DESC;
Prometheus + Grafana (Production-Standard)
# mysqld_exporter installieren
sudo apt install prometheus-mysqld-exporter
# Config: /etc/default/prometheus-mysqld-exporter
DATA_SOURCE_NAME='exporter:password@(localhost:3306)/'
# Service starten
sudo systemctl start prometheus-mysqld-exporter
# Prometheus scrapen lassen
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'mariadb'
static_configs:
- targets: ['localhost:9104']
# In Grafana: MariaDB Dashboard importieren (ID 7362)
2. Replikation: Read-Skalierung
Für KI-Systeme mit vielen Reads (Logs abrufen, Analytics) ist Master-Slave-Replikation ideal.
Setup: 1 Master + 2 Slaves
# Master-Config (/etc/mysql/mariadb.conf.d/50-server.cnf)
[mysqld]
server-id = 1
log_bin = /var/log/mysql/mariadb-bin
binlog_format = ROW
binlog_do_db = ki_system
# Replication-User erstellen
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
# Master-Status notieren
SHOW MASTER STATUS;
-- Notiere: File und Position!
# Slave-Config
[mysqld]
server-id = 2 -- Pro Slave unterschiedlich (2, 3, ...)
relay_log = /var/log/mysql/relay-bin
read_only = 1
# Slave konfigurieren
CHANGE MASTER TO
MASTER_HOST='master-ip',
MASTER_USER='repl',
MASTER_PASSWORD='repl_password',
MASTER_LOG_FILE='mariadb-bin.000001', -- Von SHOW MASTER STATUS
MASTER_LOG_POS=1234; -- Von SHOW MASTER STATUS
START SLAVE;
-- Status prüfen
SHOW SLAVE STATUS\G
-- Wichtig: Slave_IO_Running=Yes, Slave_SQL_Running=Yes
Application-Code: Writes an Master, Reads an Slaves
# PHP: Master/Slave-Trennung
class Database {
private $master;
private $slaves = [];
function __construct() {
// Master-Connection
$this->master = new PDO('mysql:host=master-ip;dbname=ki_system', ...);
// Slave-Connections
$this->slaves[] = new PDO('mysql:host=slave1-ip;dbname=ki_system', ...);
$this->slaves[] = new PDO('mysql:host=slave2-ip;dbname=ki_system', ...);
}
function write($query, $params) {
// Writes immer an Master
$stmt = $this->master->prepare($query);
return $stmt->execute($params);
}
function read($query, $params) {
// Reads an zufälligen Slave (Load-Balancing)
$slave = $this->slaves[array_rand($this->slaves)];
$stmt = $slave->prepare($query);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
3. Skalierungs-Optionen
| Methode | Komplexität | Skalierung | Use-Case |
|---|---|---|---|
| Vertikale Skalierung | Niedrig | Limitiert | Mehr RAM/CPU im Server |
| Read-Replikation | Mittel | Gut (nur Reads) | Read-Heavy Workloads |
| Sharding | Hoch | Sehr gut | Massive Datenmengen |
4. Health-Checks
# Simple Health-Check für Load-Balancer
SELECT 1;
# Erweitert: Prüfe ob Writes funktionieren
CREATE TABLE IF NOT EXISTS health_check (
last_check TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO health_check VALUES (NOW())
ON DUPLICATE KEY UPDATE last_check = NOW();
SELECT last_check FROM health_check;
5. Alerting
# Beispiel-Alerts (in Monitoring-Tool konfigurieren)
-- Alert wenn > 300 Connections
-- Alert wenn Replikation verzögert > 10 Sekunden
-- Alert wenn Disk-Space < 20%
-- Alert wenn Slow-Queries > 10% aller Queries
-- Alert wenn InnoDB Buffer Pool Hit-Rate < 95%
Aus meiner Perspektive
Production-Betrieb ist 50% Setup, 50% Monitoring. Ohne Monitoring fliegst Du blind. Ich nutze Grafana für Echtzeit-Metriken, Slow Query Log für Optimierung, tägliche Backups mit wöchentlichen Restore-Tests.
Für KI-Systeme mit hohem Log-Volumen ist Partitionierung hilfreich:
-- Logs nach Monat partitionieren
CREATE TABLE chat_logs (
id INT,
created_at TIMESTAMP,
...
) PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202501 VALUES LESS THAN (202502),
PARTITION p202502 VALUES LESS THAN (202503),
PARTITION p202503 VALUES LESS THAN (202504)
);
-- Alte Partitionen löschen (statt DELETE FROM)
ALTER TABLE chat_logs DROP PARTITION p202501;
Das ist deutlich schneller als DELETE bei Millionen Zeilen.
Kurzfazit
MariaDB ist das Rückgrat für strukturierte Daten im KI-Stack. Während Ollama Texte generiert und ChromaDB semantisch sucht, hält MariaDB die verlässliche strukturierte Datenhaltung aufrecht.
MariaDB ist das Rückgrat für strukturierte Daten im KI-Stack. Während Ollama Texte generiert und ChromaDB semantisch sucht, hält MariaDB die verlässliche strukturierte Datenhaltung aufrecht.
Die wichtigsten Punkte
- Strukturierte Daten: User, Transaktionen, Logs gehören in MariaDB
- ACID-Garantien: Kritische Daten bleiben konsistent
- Relationen: Foreign Keys verknüpfen Tabellen sinnvoll
- Prepared Statements: Schützen vor SQL-Injection
- Performance: Indizes machen langsame Queries schnell
MariaDB im KI-Stack
Nutze MariaDB für Struktur, ChromaDB für Semantik, Ollama für Intelligenz. Zusammen bilden sie ein vollständiges KI-System.
MariaDB im systemischen KI-Kontext
Nach den vorherigen Kapiteln sollte klar sein: MariaDB ist nicht nur "eine Datenbank", sondern ein fundamentaler Baustein für verlässliche KI-Systeme.
Was MariaDB leistet
Strukturierte Datenhaltung
MariaDB speichert alles, was klare Struktur und Relationen braucht:
- User-Accounts mit Permissions und Access-Levels
- Sessions mit Token-Validierung und Timeouts
- Transaktions-Logs mit atomaren Operationen
- Performance-Metriken mit Zeitreihen und Aggregationen
Datenintegrität
Durch ACID und Foreign Keys garantiert MariaDB:
- Keine verwaisten Datensätze (Foreign Keys)
- Keine inkonsistenten Zustände (Transaktionen)
- Keine Datenverluste bei Crashes (Durability)
- Keine Race-Conditions (Isolation)
Integration im KI-Stack
MariaDB arbeitet nahtlos mit Ollama und ChromaDB zusammen:
- Speichert User-IDs, die ChromaDB für Access-Control nutzt
- Loggt Ollama-Responses mit allen Metriken
- Verwaltet Embeddings-Metadaten (welches Modell, wann generiert)
- Koordiniert Workflows zwischen allen Komponenten
Wann MariaDB die richtige Wahl ist
| Anforderung | MariaDB | Begründung |
|---|---|---|
| Strukturierte Daten | ✓ Perfekt | Tabellen, Relationen, Schema |
| ACID erforderlich | ✓ Perfekt | InnoDB, Transaktionen |
| Relationen | ✓ Perfekt | Foreign Keys, JOINs |
| Semantische Suche | ✗ Nicht geeignet | Nutze ChromaDB |
| Zeitreihen-Analysen | ✓ Gut | Aggregationen, GROUP BY |
| Massive Skalierung | ~ OK | Replikation ja, Sharding komplex |
Lessons Learned
Was ich nach Jahren mit MariaDB in KI-Systemen gelernt habe:
1. Nutze jedes Tool für seine Stärke
MariaDB für Struktur, ChromaDB für Semantik, Ollama für Intelligenz. Nicht versuchen, eine Datenbank für alles zu nutzen.
2. Prepared Statements sind nicht optional
100% aller Queries mit User-Input müssen Prepared Statements nutzen. Keine Ausnahmen. SQL-Injection ist zu gefährlich.
3. Indizes machen den Unterschied
Eine Query kann 1000x schneller sein mit dem richtigen Index. EXPLAIN ist Dein Freund.
4. Backups sind nur gut wenn getestet
Monatliche Restore-Tests auf Test-Server. Zu viele Backups funktionieren nicht im Notfall.
5. Monitoring ist Pflicht
Du musst sehen: Queries/s, Slow-Queries, Connection-Count, Disk-Space. Ohne Monitoring fliegst Du blind.
Der KI-Stack: Alle Komponenten zusammen
┌──────────────────────────────────────────┐
│ User-Request (Web/API) │
└────────────────┬─────────────────────────┘
│
┌───────┴────────┐
│ PHP Backend │
└───────┬────────┘
│
┌────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌─────────┐ ┌─────────┐
│MariaDB │ │ChromaDB │ │ Ollama │
│ │ │ │ │ │
│ Users │ │ Docs │ │ LLM │
│ Logs │ │ Search │ │ Chat │
│ Metrics│ │ RAG │ │ Gen │
└────────┘ └─────────┘ └─────────┘
Aufgabenteilung:
- MariaDB: Struktur, ACID, Relationen
- ChromaDB: Semantik, Embeddings, Ähnlichkeit
- Ollama: Inferenz, Generation, Intelligenz
Aus meiner Perspektive
MariaDB ist für mich die solide Basis jedes KI-Systems. Es ist nicht das spannendste Tool (Ollama und ChromaDB sind cooler), aber es ist das verlässlichste.
In über 10 Jahren produktivem Einsatz hatte ich genau einen Datenverlust mit MariaDB (Hardware-Crash ohne Backup). Seitdem: Tägliche Backups, Binary Logs, Replikation. Null Probleme.
Für KI-Integration ist die Kombination MariaDB + ChromaDB + Ollama genau richtig. Jedes Tool macht das, was es am besten kann. Das Ergebnis: Robuste, skalierbare, intelligente Systeme.
Nächste Schritte
Wenn Du MariaDB jetzt verstehst, probiere es aus:
- Installiere MariaDB lokal
- Erstelle ein einfaches Schema (users, sessions, logs)
- Schreibe PHP/Python-Code mit Prepared Statements
- Integriere Ollama für LLM-Calls
- Füge ChromaDB für Dokumenten-Suche hinzu
- Logge alles in MariaDB
Das ist der Weg zu einem vollständigen KI-Stack. Schritt für Schritt, sauber strukturiert, produktiv einsetzbar.